This is the full developer documentation for Scalekit --- # DOCUMENT BOUNDARY --- # Implement authentication in minutes > Add MCP auth, Agent Auth, or SSO as modular components - or adopt the full-stack platform for your B2B, AI application # Implement authentication in minutes Add auth to your B2B or AI application without building from scratch. Drop in a modular capability — MCP Auth, Agent Auth, SSO, or SCIM — alongside your existing system, or adopt Scalekit as your full identity layer for users, sessions, organizations, and roles. * Claude Code Step 1 — Add the marketplace (Claude REPL) ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` Step 2 — Install your auth plugin (Claude REPL) ```bash # options: full-stack-auth, agent-auth, mcp-auth, modular-sso, modular-scim /plugin install agent-auth@scalekit-auth-stack ``` Now ask your agent to implement Scalekit auth in natural language. * Codex Step 1 — Install the Scalekit Auth Stack ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` Step 2 — Restart Codex, open **Plugin Directory**, select **Scalekit Auth Stack**, and enable your auth plugin. Now ask your agent to implement Scalekit auth in natural language. * GitHub Copilot CLI Step 1 — Add the marketplace ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` Step 2 — Install your auth plugin ```bash # options: full-stack-auth, agent-auth, mcp-auth, modular-sso, modular-scim copilot plugin install agent-auth@scalekit-auth-stack ``` Now ask your agent to implement Scalekit auth in natural language. * Cursor The Scalekit Auth Stack is pending Cursor Marketplace review. Install it locally in Cursor: Step 1 — Install the Scalekit Auth Stack ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/cursor-authstack/main/install.sh | bash ``` Step 2 — Restart Cursor, open **Settings > Cursor Settings > Plugins**, and enable your auth plugin. Now ask your agent to implement Scalekit auth in natural language. * 40+ agents Works with OpenCode, Windsurf, Cline, Gemini CLI, Codex, and 35+ more agents via the [Vercel Skills CLI](https://vercel.com/docs/agent-resources/skills). Step 1 — Browse available skills ```bash npx skills add scalekit-inc/skills --list ``` Step 2 — Install a specific skill ```bash npx skills add scalekit-inc/skills --skill adding-mcp-oauth ``` Now ask your agent to implement Scalekit auth in natural language. ## Modular Auth Add specific auth capabilities like MCP, SSO, SCIM, or Agent auth, without replacing your existing system ### [MCP Auth](/authenticate/mcp/quickstart/) [Add OAuth 2.1 authorization to your remote MCP server with Dynamic Client Registration and short-lived tokens](/authenticate/mcp/quickstart/) ### [Agent Auth](/agent-auth/quickstart/) [Give your AI agents a token vault so they can call Gmail, Slack, Notion, and other services on behalf of users](/agent-auth/quickstart/) ### [Single Sign-on](/authenticate/sso/add-modular-sso/) [Let enterprise users sign in through their company’s identity provider — Okta, Microsoft Entra, Google, and more](/authenticate/sso/add-modular-sso/) ### [SCIM Provisioning](/directory/scim/quickstart) [Automatically sync users, roles, and groups when IT admins add or remove people in Okta or Azure AD](/directory/scim/quickstart) ## FULL-STACK AUTH Use Scalekit as your full identity layer to manage users, organizations, sessions, and roles [Quickstart](/authenticate/fsa/quickstart) Get production-ready auth running in minutes ![Full Stack Auth Platform](/_astro/image-pills.uCLDErHA.svg) ### [User lifecycle](/fsa/data-modelling) [Create, update, and delete users with built-in lifecycle APIs](/fsa/data-modelling) ### [Authentication methods](/authenticate/auth-methods/passwordless/) [Support modern login flows with passkeys, magic links, OTPs, and social logins](/authenticate/auth-methods/passwordless/) ### [B2B-native identity](/fsa/data-modelling) [Model organizations, user memberships, and multi-tenant access for B2B SaaS apps](/fsa/data-modelling) ### [Authorization](/authenticate/authz/overview) [Define roles and permissions for human users and AI agents](/authenticate/authz/overview) ### [Enterprise identity](/authenticate/auth-methods/enterprise-sso) [Add enterprise capabilities like Single Sign-On (SSO) and SCIM provisioning](/authenticate/auth-methods/enterprise-sso) ### [API & M2M auth](/authenticate/m2m/api-auth-quickstart) [Issue and validate user-scoped and org-level tokens for APIs and services](/authenticate/m2m/api-auth-quickstart) ## Extensibility & Controls Customize identity workflows and apply your business logic ### [Webhooks](/reference/webhooks/overview/) [Receive real-time events for authentication, user lifecycle, and organizations](/reference/webhooks/overview/) ### [Interceptors](/authenticate/interceptors/auth-flow-interceptors/) [Apply custom logic and policy checks during authentication & authorization flows](/authenticate/interceptors/auth-flow-interceptors/) ### [Branding](/fsa/guides/login-page-branding/) [Customize hosted login/signup pages and auth-related emails to match your app’s branding](/fsa/guides/login-page-branding/) ### [Auth logs](/guides/dashboard/auth-logs/) [Record and inspect authentication events and user access activity for auditing purposes](/guides/dashboard/auth-logs/) ## Developer Resources SDKs, code samples, and community resources for building with Scalekit ### [SDKs](/apis/#description/sdks) [Drop-in libraries to quickly integrate Scalekit into your application](/apis/#description/sdks) ### [Code samples](/resources/code-samples) [Reference implementations and code examples for common auth flows](/resources/code-samples) ### [Developer community](https://join.slack.com/t/scalekit-community/shared_invite/zt-3gsxwr4hc-0tvhwT2b_qgVSIZQBQCWRw) [Ask questions, share feedback, and learn from other Scalekit developers](https://join.slack.com/t/scalekit-community/shared_invite/zt-3gsxwr4hc-0tvhwT2b_qgVSIZQBQCWRw) ### [SDKs & APIs](/sdks/) [Official SDKs and REST API reference for integrating authentication](/sdks/) ## Security, Compliance & Availability Designed for production workloads with strict operational and security requirements ⊕**Multi-region data residency**\ Dedicated regional clusters in the US and EU ⊕**Compliance**\ SOC 2, ISO 27001, GDPR, and CCPA compliant ⊕**Uptime**\ 99.99% uptime with failover redundancy ⊕**Secure token & secret storage**\ Vault-backed storage with strong isolation for tokens and agent credentials ![Compliance certifications](/_astro/compliance.G4CWsxzs.svg) --- # 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 enabling 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 ![](/.netlify/images?url=_astro%2F1-k.Cosz1iTD.png\&w=2984\&h=3570\&dpl=69cce21a4f77360008b1503a) 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. 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 This guide shows you how to set up Scalekit in your development environment. You’ll configure your workspace, get API credentials, install the SDK, verify everything works correctly, and optionally set up AI-powered development tools. 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` After installation, initialize the SDK with your credentials: * Node.js Initialize SDK ```js 1 import { Scalekit } from '@scalekit-sdk/node'; 2 3 // Initialize the Scalekit client with your credentials 4 const scalekit = new Scalekit( 5 process.env.SCALEKIT_ENVIRONMENT_URL, 6 process.env.SCALEKIT_CLIENT_ID, 7 process.env.SCALEKIT_CLIENT_SECRET 8 ); ``` * Python Initialize SDK ```python 1 from scalekit import ScalekitClient 2 import os 3 4 # Initialize the Scalekit client with your credentials 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 ) ``` * Go Initialize SDK ```go 1 import ( 2 "os" 3 "github.com/scalekit-inc/scalekit-sdk-go" 4 ) 5 6 // Initialize the Scalekit client with your credentials 7 scalekitClient := scalekit.NewScalekitClient( 8 os.Getenv("SCALEKIT_ENVIRONMENT_URL"), 9 os.Getenv("SCALEKIT_CLIENT_ID"), 10 os.Getenv("SCALEKIT_CLIENT_SECRET"), 11 ) ``` * Java Initialize SDK ```java 1 import com.scalekit.ScalekitClient; 2 3 // Initialize the Scalekit client with your credentials 4 ScalekitClient scalekitClient = new ScalekitClient( 5 System.getenv("SCALEKIT_ENVIRONMENT_URL"), 6 System.getenv("SCALEKIT_CLIENT_ID"), 7 System.getenv("SCALEKIT_CLIENT_SECRET") 8 ); ``` 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 Claude, Cursor, Windsurf, and other MCP-compatible tools. 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) Based on your MCP client, follow the configuration instructions below: * Claude Desktop 1. Open the Claude Desktop app, go to Settings, then Developer 2. Click Edit Config 3. Open the `claude_desktop_config.json` file 4. Copy and paste the server config to your existing file, then save 5. Restart Claude ```json 1 { 2 "mcpServers": { 3 "scalekit": { 4 "command": "npx", 5 "args": ["-y", "mcp-remote", "https://mcp.scalekit.com/"] 6 } 7 } 8 } ``` * Cursor 1. Open Cursor, go to Settings, then Cursor Settings 2. Select MCP on the left 3. Click Add “New Global MCP Server” at the top right 4. Copy and paste the server config to your existing file, then save 5. Restart Cursor ```json 1 { 2 "mcpServers": { 3 "scalekit": { 4 "command": "npx", 5 "args": ["-y", "mcp-remote", "https://mcp.scalekit.com/"] 6 } 7 } 8 } ``` * Windsurf 1. Open Windsurf, go to Settings, then Developer 2. Click Edit Config 3. Open the `windsurf_config.json` file 4. Copy and paste the server config to your existing file, then save 5. Restart Windsurf ```json 1 { 2 "mcpServers": { 3 "scalekit": { 4 "command": "npx", 5 "args": ["-y", "mcp-remote", "https://mcp.scalekit.com/"] 6 } 7 } 8 } ``` * VS Code (1.101+) VS Code version 1.101 or greater supports OAuth natively. Configure the MCP server directly without the `mcp-remote` proxy: ```json 1 { 2 "servers": { 3 "scalekit": { 4 "type": "http", 5 "url": "https://mcp.scalekit.com/" 6 } 7 } 8 } ``` After configuration, your MCP client will initiate an OAuth authorization workflow to securely connect to Scalekit’s MCP server. Note The Scalekit MCP server source code is available on [GitHub](https://github.com/scalekit-inc/mcp). Feel free to explore the code, raise issues, or contribute new tools. ## 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) ![](/.netlify/images?url=_astro%2Fwindsurf.CfsQQlGb.png\&w=1357\&h=818\&dpl=69cce21a4f77360008b1503a) Windsurf enables `@docs` mentions within the Cascade chat to search for the best answers to your questions. * Full Documentation ```plaintext 1 @docs:https://docs.scalekit.com/llms-full.txt 2 ``` Costs more tokens. * Specific Section ```plaintext 1 @docs:https://docs.scalekit.com/your-specific-section-or-file 2 ``` Costs less tokens. * Let AI decide ```plaintext 1 @docs:https://docs.scalekit.com/llms.txt 2 ``` 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 --- # 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 1 const { state } = req.query; 2 3 // Assumes you are using a session middleware like express-session 4 const storedState = req.session.oauthState; 5 delete req.session.oauthState; // State should be used only once 6 7 if (!state || state !== storedState) { 8 console.error('Invalid state parameter'); 9 return res.redirect('/login?error=invalid_state'); 10 } ``` * Python Validate state in Flask ```python 1 from flask import session, request, redirect 2 3 state = request.args.get('state') 4 5 # Retrieve and remove stored state from session 6 stored_state = session.pop('oauth_state', None) 7 8 if not state or state != stored_state: 9 print('Invalid state parameter') 10 return redirect('/login?error=invalid_state') ``` * Go Validate state in Gin ```go 1 stateParam := c.Query("state") 2 3 // Assumes you are using a session library like gin-contrib/sessions 4 session := sessions.Default(c) 5 storedState := session.Get("oauth_state") 6 session.Delete("oauth_state") // State should be used only once 7 session.Save() 8 9 if stateParam == "" || stateParam != storedState { 10 log.Println("Invalid state parameter") 11 c.Redirect(http.StatusFound, "/login?error=invalid_state") 12 return 13 } ``` * Java Validate state in Spring ```java 1 // Assumes HttpSession is injected into your controller method 2 String storedState = (String) session.getAttribute("oauth_state"); 3 session.removeAttribute("oauth_state"); // State should be used only once 4 5 if (state == null || !state.equals(storedState)) { 6 System.err.println("Invalid state parameter"); 7 return new RedirectView("/login?error=invalid_state"); 8 } ``` 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 1 // Use a library like 'jsonwebtoken' 2 const jwt = require('jsonwebtoken'); 3 4 // The idToken from the authResult object 5 const { idToken } = authResult; 6 7 // Decode the token without verifying its signature 8 const decoded = jwt.decode(idToken); 9 10 console.log('Decoded claims:', decoded); ``` * Python Decode ID token ```python 1 # Use a library like 'PyJWT' 2 import jwt 3 4 # The id_token from the auth_result object 5 id_token = auth_result.id_token 6 7 # Decode the token without verifying its signature 8 decoded = jwt.decode(id_token, options={"verify_signature": False}) 9 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 | 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 | 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 1 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. 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 1 const orgId = getOrganizationFromRequest(req) 2 const redirectUri = 'https://your-app.com/auth/callback' 3 const options = { 4 scopes: ['openid', 'profile', 'email', 'offline_access'], 5 organizationId: orgId, 6 } 7 const url = scalekit.getAuthorizationUrl(redirectUri, options) 8 return res.redirect(url) ``` * Python Flask ```python 1 from scalekit import AuthorizationUrlOptions 2 3 org_id = get_org_from_request(request) 4 redirect_uri = 'https://your-app.com/auth/callback' 5 options = AuthorizationUrlOptions(scopes=['openid','profile','email','offline_access'], organization_id=org_id) 6 url = scalekit_client.get_authorization_url(redirect_uri, options) 7 return redirect(url) ``` * Go Gin ```go 1 orgID := getOrgFromRequest(c) 2 redirectUri := "https://your-app.com/auth/callback" 3 options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, OrganizationId: orgID} 4 url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options) 5 c.Redirect(http.StatusFound, url.String()) ``` * Java Spring ```java 1 String orgId = getOrgFromRequest(request); 2 String redirectUri = "https://your-app.com/auth/callback"; 3 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 4 options.setScopes(Arrays.asList("openid","profile","email","offline_access")); 5 options.setOrganizationId(orgId); 6 URL url = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options); 7 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 1 const redirectUri = 'https://your-app.com/auth/callback' 2 const options = { 3 scopes: ['openid', 'profile', 'email', 'offline_access'], 4 loginHint: userEmail 5 } 6 const url = scalekit.getAuthorizationUrl(redirectUri, options) 7 return res.redirect(url) ``` * Python Flask ```python 1 redirect_uri = 'https://your-app.com/auth/callback' 2 options = AuthorizationUrlOptions(scopes=['openid','profile','email','offline_access'], login_hint=user_email) 3 url = scalekit_client.get_authorization_url(redirect_uri, options) 4 return redirect(url) ``` * Go Gin ```go 1 redirectUri := "https://your-app.com/auth/callback" 2 options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, LoginHint: userEmail} 3 url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options) 4 c.Redirect(http.StatusFound, url.String()) ``` * Java Spring ```java 1 String redirectUri = "https://your-app.com/auth/callback"; 2 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 3 options.setScopes(Arrays.asList("openid","profile","email","offline_access")); 4 options.setLoginHint(userEmail); 5 URL url = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options); 6 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 1 const redirectUri = 'https://your-app.com/auth/callback' 2 const options = { 3 scopes: ['openid', 'profile', 'email', 'offline_access'], 4 connectionId: 'conn_123...' 5 } 6 const url = scalekit.getAuthorizationUrl(redirectUri, options) 7 return res.redirect(url) ``` * Python Flask ```python 1 redirect_uri = 'https://your-app.com/auth/callback' 2 options = AuthorizationUrlOptions(scopes=['openid','profile','email','offline_access'], connection_id='conn_123...') 3 url = scalekit_client.get_authorization_url(redirect_uri, options) 4 return redirect(url) ``` * Go Gin ```go 1 redirectUri := "https://your-app.com/auth/callback" 2 options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, ConnectionId: "conn_123..."} 3 url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options) 4 c.Redirect(http.StatusFound, url.String()) ``` * Java Spring ```java 1 String redirectUri = "https://your-app.com/auth/callback"; 2 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 3 options.setScopes(Arrays.asList("openid","profile","email","offline_access")); 4 options.setConnectionId("conn_123..."); 5 URL url = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options); 6 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 1 const redirectUri = 'https://your-app.com/auth/callback' 2 const options = { 3 scopes: ['openid', 'profile', 'email', 'offline_access'], 4 prompt: 'login' 5 } 6 return res.redirect(scalekit.getAuthorizationUrl(redirectUri, options)) ``` * Python Flask ```python 1 redirect_uri = 'https://your-app.com/auth/callback' 2 options = AuthorizationUrlOptions(scopes=['openid','profile','email','offline_access'], prompt='login') 3 return redirect(scalekit_client.get_authorization_url(redirect_uri, options)) ``` * Go Gin ```go 1 redirectUri := "https://your-app.com/auth/callback" 2 options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, Prompt: "login"} 3 url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options) 4 c.Redirect(http.StatusFound, url.String()) ``` * Java Spring ```java 1 String redirectUri = "https://your-app.com/auth/callback"; 2 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 3 options.setScopes(Arrays.asList("openid","profile","email","offline_access")); 4 options.setPrompt("login"); 5 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 1 const redirectUri = 'https://your-app.com/auth/callback' 2 const options = { 3 scopes: ['openid', 'profile', 'email', 'offline_access'], 4 prompt: 'select_account' 5 } 6 return res.redirect(scalekit.getAuthorizationUrl(redirectUri, options)) ``` * Python Flask ```python 1 redirect_uri = 'https://your-app.com/auth/callback' 2 options = AuthorizationUrlOptions(scopes=['openid','profile','email','offline_access'], prompt='select_account') 3 return redirect(scalekit_client.get_authorization_url(redirect_uri, options)) ``` * Go Gin ```go 1 redirectUri := "https://your-app.com/auth/callback" 2 options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, Prompt: "select_account"} 3 url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options) 4 c.Redirect(http.StatusFound, url.String()) ``` * Java Spring ```java 1 String redirectUri = "https://your-app.com/auth/callback"; 2 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 3 options.setScopes(Arrays.asList("openid","profile","email","offline_access")); 4 options.setPrompt("select_account"); 5 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 1 const redirectUri = 'https://your-app.com/auth/callback' 2 const options = { 3 scopes: ['openid', 'profile', 'email', 'offline_access'], 4 prompt: 'create' 5 } 6 return res.redirect(scalekit.getAuthorizationUrl(redirectUri, options)) ``` * Python Flask ```python 1 redirect_uri = 'https://your-app.com/auth/callback' 2 options = AuthorizationUrlOptions(scopes=['openid','profile','email','offline_access'], prompt='create') 3 return redirect(scalekit_client.get_authorization_url(redirect_uri, options)) ``` * Go Gin ```go 1 redirectUri := "https://your-app.com/auth/callback" 2 options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, Prompt: "create"} 3 url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options) 4 c.Redirect(http.StatusFound, url.String()) ``` * Java Spring ```java 1 String redirectUri = "https://your-app.com/auth/callback"; 2 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 3 options.setScopes(Arrays.asList("openid","profile","email","offline_access")); 4 options.setPrompt("create"); 5 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 1 app.get('/login', (req, res) => { 2 const nextPath = typeof req.query.next === 'string' ? req.query.next : '/' 3 // Only allow internal paths to prevent open redirects 4 const safe = nextPath.startsWith('/') && !nextPath.startsWith('//') ? nextPath : '/' 5 res.cookie('sk_return_to', safe, { httpOnly: true, secure: true, sameSite: 'lax', path: '/' }) 6 // Build authorization URL and redirect to Scalekit 7 }) ``` * Python Flask ```python 1 @app.route('/login') 2 def login(): 3 next_path = request.args.get('next', '/') 4 safe = next_path if next_path.startswith('/') and not next_path.startswith('//') else '/' 5 resp = make_response() 6 resp.set_cookie('sk_return_to', safe, httponly=True, secure=True, samesite='Lax', path='/') 7 return resp ``` * Go Gin ```go 1 func login(c *gin.Context) { 2 nextPath := c.Query("next") 3 if nextPath == "" || !strings.HasPrefix(nextPath, "/") || strings.HasPrefix(nextPath, "//") { 4 nextPath = "/" 5 } 6 cookie := &http.Cookie{Name: "sk_return_to", Value: nextPath, HttpOnly: true, Secure: true, Path: "/"} 7 http.SetCookie(c.Writer, cookie) 8 } ``` * Java Spring ```java 1 @GetMapping("/login") 2 public void login(HttpServletRequest request, HttpServletResponse response) { 3 String nextPath = Optional.ofNullable(request.getParameter("next")).orElse("/"); 4 boolean safe = nextPath.startsWith("/") && !nextPath.startsWith("//"); 5 Cookie cookie = new Cookie("sk_return_to", safe ? nextPath : "/"); 6 cookie.setHttpOnly(true); cookie.setSecure(true); cookie.setPath("/"); 7 response.addCookie(cookie); 8 } ``` **Step 2: Redirect after callback** After exchanging the authorization code, read the cookie and redirect to the stored path: * Node.js Express.js ```javascript 1 app.get('/auth/callback', async (req, res) => { 2 // ... exchange code ... 3 const raw = req.cookies.sk_return_to || '/' 4 const safe = raw.startsWith('/') && !raw.startsWith('//') ? raw : '/' 5 res.clearCookie('sk_return_to', { path: '/' }) 6 res.redirect(safe || '/dashboard') 7 }) ``` * Python Flask ```python 1 def callback(): 2 # ... exchange code ... 3 raw = request.cookies.get('sk_return_to', '/') 4 safe = raw if raw.startswith('/') and not raw.startswith('//') else '/' 5 resp = redirect(safe or '/dashboard') 6 resp.delete_cookie('sk_return_to', path='/') 7 return resp ``` * Go Gin ```go 1 func callback(c *gin.Context) { 2 // ... exchange code ... 3 raw, _ := c.Cookie("sk_return_to") 4 if raw == "" || !strings.HasPrefix(raw, "/") || strings.HasPrefix(raw, "//") { 5 raw = "/" 6 } 7 http.SetCookie(c.Writer, &http.Cookie{Name: "sk_return_to", Value: "", MaxAge: -1, Path: "/"}) 8 c.Redirect(http.StatusFound, raw) 9 } ``` * Java Spring ```java 1 public RedirectView callback(HttpServletRequest request, HttpServletResponse response) { 2 // ... exchange code ... 3 String raw = getCookie(request, "sk_return_to").orElse("/"); 4 boolean ok = raw.startsWith("/") && !raw.startsWith("//"); 5 Cookie clear = new Cookie("sk_return_to", ""); clear.setPath("/"); clear.setMaxAge(0); 6 response.addCookie(clear); 7 return new RedirectView(ok ? raw : "/dashboard"); 8 } ``` 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 Recommended Use `login` to force re-authentication Use `create` to trigger sign up page Use `select_account` to select an account if they have multiple accounts | | `organization_id` | Route user to specific organization’s configured authentication method Optional | | `connection_id` | Skip organization selection and direct user to specific SSO connection Optional | | `login_hint` | Pre-populate the email field with a hint Optional Useful for domain-based routing when combined with `organization_id` | ## 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 1 // Generate secure random state 2 const state = require('crypto').randomBytes(32).toString('hex'); 3 // Store it temporarily (session, local storage, cache, etc) 4 sessionStorage.oauthState = state; ``` * Python Generate and store state ```python 1 import os 2 import secrets 3 4 # Generate secure random state 5 state = secrets.token_hex(32) 6 # Store it temporarily (session, local storage, cache, etc) 7 session['oauth_state'] = state ``` * Go Generate and store state ```go 1 import ( 2 "crypto/rand" 3 "encoding/hex" 4 ) 5 6 // Generate secure random state 7 b := make([]byte, 32) 8 rand.Read(b) 9 state := hex.EncodeToString(b) 10 // Store it temporarily (session, local storage, cache, etc) 11 // Example for Go: use a storage library 12 // session.Set("oauth_state", state) ``` * Java Generate and store state ```java 1 import java.security.SecureRandom; 2 import java.util.Base64; 3 4 // Generate secure random state 5 SecureRandom sr = new SecureRandom(); 6 byte[] randomBytes = new byte[32]; 7 sr.nextBytes(randomBytes); 8 String state = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); 9 // Store it temporarily (session, local storage, cache, etc) 10 // Example for Java: use any storage library 11 // 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 1 import { Scalekit } from '@scalekit-sdk/node'; 2 3 const scalekit = new Scalekit(/* your credentials */); 4 5 // Basic authorization URL for general login 6 const redirectUri = 'https://yourapp.com/auth/callback'; 7 const options = { 8 scopes: ['openid', 'profile', 'email', 'offline_access'], 9 state: sessionStorage.oauthState, 10 }; 11 12 const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options); 13 14 // Redirect user to Scalekit's hosted login page 15 res.redirect(authorizationUrl); ``` * Python Flask ```python 3 collapsed lines 1 from scalekit import ScalekitClient, AuthorizationUrlOptions 2 3 scalekit = ScalekitClient(/* your credentials */) 4 5 # Basic authorization URL for general login 6 redirect_uri = 'https://yourapp.com/auth/callback' 7 options = AuthorizationUrlOptions( 8 scopes=['openid', 'profile', 'email', 'offline_access'], 9 state=session['oauth_state'] # Add this line 10 ) 11 12 authorization_url = scalekit.get_authorization_url(redirect_uri, options) 13 14 # Redirect user to Scalekit's hosted login page 15 return redirect(authorization_url) ``` * Go Gin ```go 4 collapsed lines 1 import "github.com/scalekit-inc/scalekit-sdk-go" 2 3 scalekit := scalekit.NewScalekitClient(/* your credentials */) 4 5 // Basic authorization URL for general login 6 redirectUri := "https://yourapp.com/auth/callback" 7 options := scalekit.AuthorizationUrlOptions{ 8 Scopes: []string{"openid", "profile", "email", "offline_access"}, 9 State: "your_generated_state", // Add this line 10 } 11 12 authorizationUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options) 13 14 // Redirect user to Scalekit's hosted login page 15 c.Redirect(http.StatusFound, authorizationUrl.String()) ``` * Java Spring ```java 4 collapsed lines 1 import com.scalekit.ScalekitClient; 2 import com.scalekit.internal.http.AuthorizationUrlOptions; 3 4 ScalekitClient scalekit = new ScalekitClient(/* your credentials */); 5 6 // Basic authorization URL for general login 7 String redirectUri = "https://yourapp.com/auth/callback"; 8 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 9 options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access")); 10 options.setState("your_generated_state"); // Add this line 11 12 URL authorizationUrl = scalekit.authentication().getAuthorizationUrl(redirectUri, options); 13 14 // Redirect user to Scalekit's hosted login page 15 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) Cases where your app wants to keep the sign up flow seperate and dedicated to creating the user account, you can use the `prompt: 'create'` parameter to redirect the user to the sign up page. * Node.js Express.js ```diff 1 const redirectUri = 'http://localhost:3000/api/callback'; 2 const options = { 3 scopes: ['openid', 'profile', 'email', 'offline_access'], 4 prompt: 'create', // explicitly takes you to sign up flow 5 }; 4 collapsed lines 6 7 const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options); 8 9 res.redirect(authorizationUrl); ``` * Python Flask ```diff 1 from scalekit import AuthorizationUrlOptions 2 3 redirect_uri = 'http://localhost:3000/api/callback' 4 options = AuthorizationUrlOptions() 5 options.scopes=['openid', 'profile', 'email', 'offline_access'] 6 options.prompt='create' # optional: explicitly takes you to sign up flow 7 4 collapsed lines 8 authorization_url = scalekit.get_authorization_url(redirect_uri, options) 9 10 # For web frameworks like Flask/Django: 11 # return redirect(authorization_url) ``` * Go Gin ```diff 1 redirectUri := "http://localhost:3000/api/callback" 2 options := scalekit.AuthorizationUrlOptions{ 3 Scopes: []string{"openid", "profile", "email", "offline_access"}, 4 +Prompt: "create", // explicitly takes you to sign up flow 5 } 6 8 collapsed lines 7 authorizationUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options) 8 if err != nil { 9 // handle error appropriately 10 panic(err) 11 } 12 13 // For web frameworks like Gin: 14 // c.Redirect(http.StatusFound, authorizationUrl.String()) ``` * Java Spring ```diff 4 collapsed lines 1 import com.scalekit.internal.http.AuthorizationUrlOptions; 2 import java.net.URL; 3 import java.util.Arrays; 4 5 String redirectUri = "http://localhost:3000/api/callback"; 6 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 7 options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access")); 8 +options.setPrompt("create"); 9 10 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 --- # Quickstart: Add auth to your AI agents > Set up connectors and get OAuth tokens and call tools on the fly using Scalekit's Agent Auth Users ask your app in natural language - “Show me last 5 unread emails” or “Create a calendar event for tomorrow” or “Send an alert to my team” All of these actions require your app to be able to securely connect to third-party applications like Gmail, Calendar, Slack, Notion etc. and execute actions on behalf of the user. Scalekit handles: * Authorizing your application with 3rd party apps. * Allows you to configure a connection with each 3rd party app using your own credentials or using Scalekit’s credentials (for faster development). * Every connection would maintain token vault allowing you to fetch tokens and use them to make API calls. * Provides a unified API to call tools on behalf of the user. Scalekit provides tools if your preferred connector is not supported. * Manage each user account per connector through Scalekit dashboard or programmatically. ### Build with a coding agent * Claude Code ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` ```bash /plugin install agent-auth@scalekit-auth-stack ``` * Codex ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` ```bash # Restart Codex # Plugin Directory -> Scalekit Auth Stack -> install agent-auth ``` * GitHub Copilot CLI ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` ```bash copilot plugin install agent-auth@scalekit-auth-stack ``` * 40+ agents ```bash npx skills add scalekit-inc/skills --skill integrating-agent-auth ``` [Continue building with AI →](/dev-kit/build-with-ai/agent-auth/) ## Agent that fetches last 5 unread emails [Section titled “Agent that fetches last 5 unread emails”](#agent-that-fetches-last-5-unread-emails) In this quickstart, you’ll build a simple tool-calling program that: 1. **Authenticates a user with Gmail** to authenticate their Gmail account over OAuth 2.0 2. **Fetches last 5 unread emails** from the user’s inbox 1) ## Set up your environment [Section titled “Set up your environment”](#set-up-your-environment) Get Your credentials from Scalekit dashboard at [app.scalekit.com](https://app.scalekit.com) -> Developers-> Settings -> API Credentials Install the Scalekit SDK for your preferred language and initialize the client with your API credentials: * Python ```sh pip install scalekit-sdk-python python-dotenv requests ``` * Node.js ```sh npm install @scalekit-sdk/node ``` - Python ```python import scalekit.client import os import requests 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_ENV_URL"), ) actions = scalekit_client.actions ``` - Node.js ```typescript import { ScalekitClient } from '@scalekit-sdk/node'; import { ConnectorStatus } from '@scalekit-sdk/node/lib/pkg/grpc/scalekit/v1/connected_accounts/connected_accounts_pb'; import 'dotenv/config'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENV_URL, process.env.SCALEKIT_CLIENT_ID, process.env.SCALEKIT_CLIENT_SECRET ); const actions = scalekit.actions; ``` 2) ## Create connected account for the user [Section titled “Create connected account for the user”](#create-connected-account-for-the-user) Connected account is a user’s 3rd party account that is registered in your Scalekit environment. This represents the user’s connection to their Gmail account in this case. * Python ```python # Create or retrieve the user's connected Gmail account response = actions.get_or_create_connected_account( connection_name="gmail", identifier="user_123" # Replace with your system's unique user ID ) connected_account = response.connected_account print(f'Connected account created: {connected_account.id}') ``` * Node.js ```typescript // Create or retrieve the user's connected Gmail account const response = await actions.getOrCreateConnectedAccount({ connectionName: 'gmail', identifier: 'user_123', // Replace with your system's unique user ID }); const connectedAccount = response.connectedAccount; console.log('Connected account created:', connectedAccount?.id); ``` Set up a connection The Gmail connector is enabled by default for quick getting started purposes. In case your app wants to connect to different third-party apps, you can setup a connection for each app in the Scalekit dashboard > Agent Auth > *+ Create Connection* To set up your app’s credentials for a provider, check the **Providers** section in the sidebar. 3) ## Authenticate the user [Section titled “Authenticate the user”](#authenticate-the-user) In order to execute any tools on behalf of the user, the user first needs to grant authorization to access their gmail account. Scalekit automatically handles the entire OAuth workflow with the Gmail provider, gets the access token, refreshes the access token periodically based on the refresh token etc. If the user’s access token is expired, Scalekit will automatically refresh the access token using the refresh token. At any time, you can check the authorization status of the connected account and determine if the user needs to re-authorize the connection. * Python ```python # Generate authorization link if user hasn't authorized or token is expired if(connected_account.status != "ACTIVE"): print(f"Gmail is not connected: {connected_account.status}") link_response = actions.get_authorization_link( connection_name="gmail", identifier="user_123" ) print(f"🔗 click on the link to authorize Gmail", link_response.link) input(f"⎆ Press Enter after authorizing Gmail...") # In production, redirect user to this URL to complete OAuth flow ``` * Node.js ```typescript // Generate authorization link if user hasn't authorized or token is expired if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { console.log('gmail is not connected:', connectedAccount?.status); const linkResponse = await actions.getAuthorizationLink({ connectionName: 'gmail', identifier: 'user_123', }); console.log('🔗 click on the link to authorize gmail', linkResponse.link); // In production, redirect user to this URL to complete OAuth flow } ``` 4) ## Call the Gmail API to fetch last 5 unread emails [Section titled “Call the Gmail API to fetch last 5 unread emails”](#call-the-gmail-api-to-fetch-last-5-unread-emails) Now that the user is authenticated, use Scalekit’s proxy to call the Gmail API directly — no need to manage access tokens yourself. * Python ```python # Fetch 5 unread emails via Scalekit proxy result = actions.request( connection_name="gmail", identifier="user_123", path="/gmail/v1/users/me/messages", method="GET", params={"q": "is:unread", "maxResults": 5} ) print(result) ``` * Node.js ```typescript // Fetch 5 unread emails via Scalekit proxy const result = await actions.request({ connectionName: 'gmail', identifier: 'user_123', path: '/gmail/v1/users/me/messages', method: 'GET', queryParams: { q: 'is:unread', maxResults: 5 }, }); console.log(result); ``` 5) ## Use Scalekit optimized tools [Section titled “Use Scalekit optimized tools”](#use-scalekit-optimized-tools) In addition to the proxy, Scalekit provides optimized tools built specifically for AI agents. These tools abstract away API complexity — no need to know the exact endpoint, parameters, or response shape. Under the hood, they orchestrate multiple API calls and return the response in a structured, AI-friendly format that’s easy for your agent to reason over. * Python ```python response = actions.execute_tool( tool_name="gmail_fetch_mails", identifier="user_123", tool_input={ "query": "is:unread", "max_results": 5, }, ) print(response) ``` * Node.js ```typescript const toolResponse = await actions.executeTool({ toolName: 'gmail_fetch_mails', connectedAccountId: connectedAccount?.id, toolInput: { query: 'is:unread', max_results: 5, }, }); console.log('Recent emails:', toolResponse.data); ``` Congratulations! You’ve successfully implemented a program that executes API calls on behalf of a user using Agent Auth. This can be extended to any number of connections and Scalekit handles the authentication and token management for you. --- # 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. ### Build with a coding agent * Claude Code ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` ```bash /plugin install mcp-auth@scalekit-auth-stack ``` * Codex ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` ```bash # Restart Codex # Plugin Directory -> Scalekit Auth Stack -> install mcp-auth ``` * GitHub Copilot CLI ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` ```bash copilot plugin install mcp-auth@scalekit-auth-stack ``` * 40+ agents ```bash npx skills add scalekit-inc/skills --skill adding-mcp-oauth ``` [Continue building with AI →](/dev-kit/build-with-ai/mcp-auth/) 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=69cce21a4f77360008b1503a) 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) * **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=69cce21a4f77360008b1503a) 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). * 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 = 'https://your-mcp-server.com'; // If no Server URL is set in Scalekit, use the autogenerated resource ID (e.g., res_123456789) from your dashboard. 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 = "https://your-mcp-server.com" # If no Server URL is set in Scalekit, use the autogenerated resource ID (e.g., res_123456789) from your dashboard. 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 1 // Extract Bearer token from Authorization header 2 // Use case: Validate requests from AI hosts like Claude Desktop, Cursor, or VS Code 3 const authHeader = req.headers['authorization']; 4 const token = authHeader?.startsWith('Bearer ') 5 ? authHeader.split('Bearer ')[1]?.trim() 6 : null; 7 8 if (!token) { 9 throw new Error('Missing or invalid Bearer token'); 10 } ``` * Python ```python 1 # Extract Bearer token from Authorization header 2 # Use case: Validate requests from AI hosts like Claude Desktop, Cursor, or VS Code 3 auth_header = request.headers.get("Authorization", "") 4 token = None 5 if auth_header.startswith("Bearer "): 6 token = auth_header.split("Bearer ")[1].strip() 7 8 if not token: 9 raise ValueError("Missing or invalid Bearer token") ``` Validate the token against your configured resource audience to ensure it was issued for your specific MCP server. The resource identifier must match the Server URL you registered earlier. * Node.js Validate token ```javascript 1 // Security: Validate token against configured resource audience 2 // This ensures the token was issued for your specific MCP server 3 await scalekit.validateToken(token, { 4 issuer: '' 5 audience: [RESOURCE_ID] 6 }); ``` * Python Validate token ```python 1 # Method 1: validate_access_token - Returns boolean (True/False) 2 # Use this method when you only need to verify token validity without detailed error information. 3 # This approach is suitable for simple authorization checks where you don't need token claims. 4 def validate_token_with_issuer_audience(token: str) -> bool: 5 """ 6 Validates a token and returns True if valid, False otherwise. 7 8 :param token: The token to validate 9 :return: True if token is valid, False otherwise 10 """ 11 options = TokenValidationOptions( 12 issuer="", 13 audience=[RESOURCE_ID] 14 ) 15 16 try: 17 is_valid = scalekit_client.validate_access_token(token, options=options) 18 return is_valid 19 except Exception as ex: 20 print(f"Token validation failed: {ex}") 21 return False 22 23 # Method 2: validate_token - Returns token claims/payload 24 # Use this method when you need access to token claims (user info, scopes, etc.) or detailed error information. 25 # This approach is suitable for authorization that requires specific user context or scope validation. 26 def validate_token_and_get_claims(token: str) -> dict: 27 """ 28 Validates a token with specific audience and raises exception on failure. 29 30 :param token: The token to validate 31 :raises: ScalekitValidateTokenFailureException if validation fails 32 """ 33 options = TokenValidationOptions( 34 issuer="", 35 audience=[RESOURCE_ID], 36 required_scopes=["todo:read", "todo:write"] # Optional: validate specific scopes for finer access control 37 ) 38 39 scalekit_client.validate_token(token, options=options) ``` #### 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 = 'https://your-mcp-server.com'; // If no Server URL is set in Scalekit, use the autogenerated resource ID (e.g., res_123456789) from your dashboard. 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 await scalekit.validateToken(token, { 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 = "https://your-mcp-server.com" # If no Server URL is set in Scalekit, use the autogenerated resource ID (e.g., res_123456789) from your dashboard. 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: options = TokenValidationOptions( issuer=os.getenv("SCALEKIT_ENVIRONMENT_URL"), 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](/agent-auth/advanced/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 Quickstart](/.netlify/images?url=_astro%2Fscim-chart.D8FO-9f1.png\&w=5776\&h=1924\&dpl=69cce21a4f77360008b1503a) ### Build with a coding agent * Claude Code ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` ```bash /plugin install modular-scim@scalekit-auth-stack ``` * Codex ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` ```bash # Restart Codex # Plugin Directory -> Scalekit Auth Stack -> install modular-scim ``` * GitHub Copilot CLI ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` ```bash copilot plugin install modular-scim@scalekit-auth-stack ``` * 40+ agents ```bash npx skills add scalekit-inc/skills --skill implementing-scim-provisioning ``` [Continue building with AI →](/dev-kit/build-with-ai/scim/) ## 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` Navigate to **Dashboard > Developers > Settings > API Credentials** to obtain your credentials. Store your credentials securely in environment variables: .env ```shell 1 # Get these values from Dashboard > Developers > Settings > API Credentials 2 SCALEKIT_ENVIRONMENT_URL='https://b2b-app-dev.scalekit.com' 3 SCALEKIT_CLIENT_ID='' 4 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 1 # Security: Replace with a valid access token from Scalekit 2 # This token authorizes your API requests to access organization data 3 4 # Use case: Verify API connectivity and test authentication 5 # Examples: Initial setup testing, debugging integration issues 6 7 curl -L "https://$SCALEKIT_ENVIRONMENT_URL/api/v1/organizations?page_size=5" \ 8 -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 1 try { 2 // Use case: Get specific directory when organization has multiple directories 3 // Examples: Department-specific provisioning, multi-division companies 4 const { directory } = await scalekit.directory.getDirectory('', ''); 5 console.log(`Directory name: ${directory.name}`); 6 7 // Use case: Get primary directory for simple provisioning workflows 8 // Examples: Small organizations, single-directory setups 9 const { directory } = await scalekit.directory.getPrimaryDirectoryByOrganizationId(''); 10 console.log(`Primary directory ID: ${directory.id}`); 11 } catch (error) { 12 console.error('Failed to retrieve directory:', error); 13 // Handle error appropriately for your application 14 } ``` * 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 1 // Use case: Retrieve specific directory for granular access control 2 // Examples: Multi-tenant environments, department-level provisioning 3 directory, err := scalekitClient.Directory().GetDirectory(ctx, organizationId, directoryId) 4 if err != nil { 5 return fmt.Errorf("failed to get directory: %w", err) 6 } 7 fmt.Printf("Directory name: %s\n", directory.Name) 8 9 // Use case: Get primary directory for simplified user management 10 // Examples: Automated provisioning workflows, bulk user imports 11 directory, err := scalekitClient.Directory().GetPrimaryDirectoryByOrganizationId(ctx, organizationId) 12 if err != nil { 13 return fmt.Errorf("failed to get primary directory: %w", err) 14 } 15 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 1 try { 2 // Use case: Bulk user synchronization and provisioning 3 // Examples: New customer onboarding, scheduled user data sync 4 const { users } = await scalekit.directory.listDirectoryUsers('', ''); 5 6 // Process each user for provisioning or updates 7 users.forEach(user => { 8 console.log(`User email: ${user.email}, Name: ${user.name}`); 9 // TODO: Implement your user provisioning logic here 10 }); 11 } catch (error) { 12 console.error('Failed to list directory users:', error); 13 // Handle error appropriately for your application 14 } ``` * Python Python ```python 1 try: 2 # Use case: Automated user provisioning workflows 3 # Examples: HR system integration, bulk user imports 4 directory_users = scalekit_client.directory.list_directory_users( 5 organization_id='', directory_id='' 6 ) 7 8 # Process each user for local database updates 9 for user in directory_users: 10 print(f'User email: {user.email}, Name: {user.name}') 11 # TODO: Implement your user synchronization logic here 12 except Exception as error: 13 print(f'Error listing directory users: {error}') 14 # 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. 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 1 try: 2 # Use case: Department-based access control 3 # Examples: Engineering vs Sales permissions, project team access 4 directory_groups = scalekit_client.directory.list_directory_groups( 5 directory_id='', organization_id='' 6 ) 7 8 # Process each group for permission mapping 9 for group in directory_groups: 10 print(f'Group name: {group.name}, ID: {group.id}') 11 # TODO: Implement your group-based permission logic here 12 except Exception as error: 13 print(f'Error listing directory groups: {error}') 14 # 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('/webhook', async (req, res) => { 2 // Security: ALWAYS verify requests are from Scalekit before processing 3 // This prevents unauthorized parties from triggering your provisioning logic 4 5 const event = req.body; 6 const headers = req.headers; 7 const secret = process.env.SCALEKIT_WEBHOOK_SECRET; 8 9 try { 10 // Verify webhook signature to prevent replay attacks and forged requests 11 await scalekit.verifyWebhookPayload(secret, headers, event); 12 } catch (error) { 13 console.error('Webhook signature verification failed:', error); 14 // Return 400 for invalid signatures - this prevents processing malicious requests 15 return res.status(400).json({ error: 'Invalid signature' }); 16 } 17 18 try { 19 // Use case: Real-time user provisioning based on directory events 20 // Examples: New hire onboarding, emergency access revocation, role changes 21 const { email, name } = event.data; 22 23 // Process the webhook event based on its type 24 switch (event.type) { 25 case 'organization.directory.user_created': 26 await createUserAccount(email, name); 27 break; 28 case 'organization.directory.user_updated': 29 await updateUserAccount(email, name); 30 break; 31 case 'organization.directory.user_deleted': 32 await deactivateUserAccount(email); 33 break; 34 default: 35 console.log(`Unhandled event type: ${event.type}`); 36 } 37 38 res.status(201).json({ message: 'Webhook processed successfully' }); 39 } catch (processingError) { 40 console.error('Failed to process webhook event:', processingError); 41 res.status(500).json({ error: 'Processing failed' }); 42 } 43 }); ``` * 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 headers = request.headers 13 body = await request.json() 14 15 try: 16 # Verify webhook payload using the secret from Scalekit dashboard 17 # Get this from Dashboard > Webhooks after registering your endpoint 18 is_valid = scalekit_client.verify_webhook_payload( 19 secret=os.getenv("SCALEKIT_WEBHOOK_SECRET"), 20 headers=headers, 21 payload=json.dumps(body).encode('utf-8') 22 ) 23 24 if not is_valid: 25 raise HTTPException(status_code=400, detail="Invalid webhook signature") 26 27 except Exception as verification_error: 28 print(f"Webhook verification failed: {verification_error}") 29 raise HTTPException(status_code=400, detail="Webhook verification failed") 30 31 # Use case: Instant user provisioning based on directory events 32 # Examples: Automated onboarding, immediate access revocation, role updates 33 try: 34 event_type = body.get("type") 35 event_data = body.get("data", {}) 36 email = event_data.get("email") 37 name = event_data.get("name") 38 39 if event_type == "organization.directory.user_created": 40 await create_user_account(email, name) 41 elif event_type == "organization.directory.user_updated": 42 await update_user_account(email, name) 43 elif event_type == "organization.directory.user_deleted": 44 await deactivate_user_account(email) 45 46 return JSONResponse(status_code=201, content={"status": "processed"}) 47 48 except Exception as processing_error: 49 print(f"Failed to process webhook: {processing_error}") 50 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 try { 12 // Verify webhook signature using Scalekit SDK 13 boolean isValid = scalekitClient.webhook() 14 .verifyWebhookPayload(secret, headers, body.getBytes()); 15 16 if (!isValid) { 17 return ResponseEntity.badRequest().body("Invalid webhook signature"); 18 } 19 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 _, err = scalekitClient.VerifyWebhookPayload(webhookSecret, headers, body) 30 if err != nil { 31 http.Error(w, "Invalid webhook signature", http.StatusBadRequest) 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 deactivation * `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: Webhook event payload ```json 1 { 2 "id": "evt_1234567890", 3 "type": "organization.directory.user_created", 4 "data": { 5 "email": "john.doe@company.com", 6 "name": "John Doe", 7 "organization_id": "org_12345", 8 "directory_id": "dir_67890" 9 }, 10 "timestamp": "2024-01-15T10:30:00Z" 11 } ``` Webhook delivery and retry policy Scalekit attempts webhook delivery using an exponential backoff retry policy until we receive a successful 200/201 response code from your servers: | Attempt | Timing | | ------- | ----------- | | 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 --- # Modular SSO quickstart > 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 their OIDC and SAML protocols, you can let Scalekit handle those connections with each of your customer’s identity providers. 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=69cce21a4f77360008b1503a) 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 complete authentication? [Complete authentication](/authenticate/fsa/quickstart/) includes SSO functionality by default. If you’re using complete authentication, you can skip this guide. ### Build with a coding agent * Claude Code ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` ```bash /plugin install modular-sso@scalekit-auth-stack ``` * GitHub Copilot CLI ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` ```bash copilot plugin install modular-sso@scalekit-auth-stack ``` * 40+ agents ```bash npx skills add scalekit-inc/skills --skill modular-sso ``` [Continue building with AI →](/dev-kit/build-with-ai/sso/) 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` Since we will using Modular SSO, you need to disable complete auth: 1. Go to Dashboard > Authentication > General 2. Under “Full-Stack Auth” section, click “Disable Full-Stack Auth” Now you’re ready to start integrating SSO into your app! 2. ## 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) Use the Scalekit SDK to construct authorization URL with your redirect URI and required scopes. Scalekit will automatically redirect the user to the user’s enterprise identity provider login page to authenticate. * 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& # "offline_access" is required to receive a refresh token 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. If your application needs to verify whether an SSO connection exists for a specific domain before proceeding, you can use the [list connections by domain SDK method](/guides/user-auth/check-sso-domain/). For details on how Scalekit determines which SSO connection to use, refer to the [SSO identifier precedence rules](/sso/guides/authorization-url/#parameter-precedence). 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 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 1 // Validate and decode the ID token from the authentication result 2 const idTokenClaims = await scalekit.validateToken(result.idToken); 3 4 // Validate and decode the access token 5 const accessTokenClaims = await scalekit.validateToken(result.accessToken); ``` * Python Validate tokens ```py 1 # Validate and decode the ID token from the authentication result 2 id_token_claims = scalekit_client.validate_token(result["id_token"]) 3 4 # Validate and decode the access token 5 access_token_claims = scalekit_client.validate_token(result["access_token"]) ``` * Go Validate tokens ```go 1 // Create a background context for the API call 2 ctx := context.Background() 3 4 // Validate and decode the access token (uses JWKS from the client) 5 accessTokenClaims, err := scalekitClient.GetAccessTokenClaims(ctx, result.AccessToken) 6 if err != nil { 7 // handle error 8 } ``` * Java Validate tokens ```java 1 // Validate and decode the ID token 2 Map idTokenClaims = scalekitClient.validateToken(result.getIdToken()); 3 4 // Validate and decode the access token 5 Map accessTokenClaims = scalekitClient.validateToken(result.getAccessToken()); ``` - Auth result ```js 1 { 2 user: { 3 email: 'john@example.com', 4 familyName: 'Doe', 5 givenName: 'John', 6 username: 'john@example.com', 7 id: 'conn_70087756662964366;dcc62570-6a5a-4819-b11b-d33d110c7716' 8 }, 9 idToken: 'eyJhbGciOiJSU..bcLQ', 10 accessToken: 'eyJhbGciO..', 11 expiresIn: 899 12 } ``` - ID token (decoded) ```js 1 { 2 iss: '', // Issuer: Scalekit environment URL (must match your environment) 3 aud: [ 'skc_70087756327420046' ], // Audience: Your client ID (must match for validation) 4 azp: 'skc_70087756327420046', // Authorized party: Usually same as aud 5 sub: 'conn_70087756662964366;e964d135-35c7-4a13-a3b4-2579a1cdf4e6', // Subject: Connection ID and IdP user ID (SSO-specific format) 6 oid: 'org_70087756646187150', // Organization ID: User's organization 7 exp: 1758952038, // Expiration: Unix timestamp (validate token hasn't expired) 8 iat: 1758692838, // Issued at: Unix timestamp when token was issued 9 at_hash: 'yMGIBg7BkmIGgD6_dZPEGQ', // Access token hash: For token binding validation 10 c_hash: '4x7qsXnlRw6dRC6twnuENw', // Authorization code hash: For code binding validation 11 amr: [ 'conn_70087756662964366' ], // Authentication method reference: SSO connection ID used for authentication 12 email: 'john@example.com', // User's email address 13 preferred_username: 'john@example.com', // Preferred username (often same as email for SSO) 14 family_name: 'Doe', // User's last name 15 given_name: 'John', // User's first name 16 sid: 'ses_91646612652163629', // Session ID: Links token to user session 17 client_id: 'skc_70087756327420046' // Client ID: Your application identifier 18 } ``` - Access token (decoded) ```js 1 { 2 "iss": "", // Issuer: Scalekit environment URL (must match your environment) 3 "aud": ["skc_70087756327420046"], // Audience: Your client ID (must match for validation) 4 "sub": "conn_70087756662964366;dcc62570-6a5a-4819-b11b-d33d110c7716", // Subject: Connection ID and IdP user ID (SSO-specific format) 5 "exp": 1758693916, // Expiration: Unix timestamp (validate token hasn't expired) 6 "iat": 1758693016, // Issued at: Unix timestamp when token was issued 7 "nbf": 1758693016, // Not before: Unix timestamp (token valid from this time) 8 "jti": "tkn_91646913048216109", // JWT ID: Unique token identifier 9 "client_id": "skc_70087756327420046" // Client ID: Your application identifier 10 } ``` 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. ## 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=69cce21a4f77360008b1503a) For detailed testing instructions and scenarios, see our [Complete SSO testing guide](/sso/guides/test-sso/) 6. ## 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) 7. ## 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. Learn more: [Embedded Admin Portal guide](/guides/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.connections.listConnectionsByDomain({ 6 domain 7 }); 8 9 if (connections.length > 0) { 10 // Domain has SSO configured - redirect to identity provider 11 const authUrl = scalekit.getAuthorizationUrl(redirectUri, { 12 domainHint: domain // Automatically routes to correct IdP 13 }); 14 return res.redirect(authUrl); 15 } else { 16 // No SSO for this domain - show alternative login methods 17 return showPasswordlessLogin(); 18 } ``` * 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.connections.list_connections_by_domain( 6 domain=domain 7 ) 8 9 if len(connections) > 0: 10 # Domain has SSO configured - redirect to identity provider 11 options = AuthorizationUrlOptions() 12 options.domain_hint = domain # Automatically routes to correct IdP 13 14 auth_url = scalekit_client.get_authorization_url( 15 redirect_uri=redirect_uri, 16 options=options 17 ) 18 return redirect(auth_url) 19 else: 20 # No SSO for this domain - show alternative login methods 21 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 --- # 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 --- # 404 > Wrong endpoint, right universe. Let's get you back on track. Something broken on our end? Check the [Status page](https://scalekit.statuspage.io/). --- # DOCUMENT BOUNDARY --- # Bring your own Credentials > Learn how to use your own OAuth authentication credentials with Agent Auth for complete whitelabeling and control. Bring Your Own Credentials (BYOC) allows you to use your own OAuth applications and authentication credentials with Agent Auth instead of Scalekit’s shared credentials. This provides complete control over the authentication experience and enables full whitelabeling of your application. ## Why bring your own authentication? [Section titled “Why bring your own authentication?”](#why-bring-your-own-authentication) ### Complete whitelabeling [Section titled “Complete whitelabeling”](#complete-whitelabeling) When you use your own OAuth credentials, users see your application name and branding throughout the authentication flow instead of Scalekit’s: * **OAuth consent screens** display your app name and logo * **Authorization URLs** use your domain and branding * **Email notifications** from providers reference your application * **User permissions** are granted directly to your application ### Enhanced security and control [Section titled “Enhanced security and control”](#enhanced-security-and-control) * **Direct relationship**: Maintain direct OAuth relationships with providers * **Full audit trail**: Complete visibility into authentication flows and user consent * **Custom verification**: Complete OAuth app verification with your company details * **Compliance control**: Meet regulatory requirements for direct provider relationships ### Production-grade capabilities [Section titled “Production-grade capabilities”](#production-grade-capabilities) * **Dedicated quotas**: Avoid sharing rate limits with other Scalekit customers * **Higher limits**: Access provider-specific quota increases for your application * **Priority support**: Direct support relationships with OAuth providers * **Custom integrations**: Build provider-specific customizations ## How BYOC works [Section titled “How BYOC works”](#how-byoc-works) ### Architecture overview [Section titled “Architecture overview”](#architecture-overview) With BYOC, authentication flows work as follows: 1. **Scalekit** handles the initial authentication request with your OAuth client-id details 2. **Provider** authenticates the user and returns tokens to Scalekit 3. **Agent Auth** uses your tokens to execute tools on behalf of users ## Setting up BYOC [Section titled “Setting up BYOC”](#setting-up-byoc) Login to Scalekit Dashboard and Click on Edit Connection for the application you want to configure your own authentication credentials. Choose the option “Use your own credentials” and enter the Client ID and Client Secret obtained from the provider. Copy the Redirect URL you find in the Scalekit Dashboard and add it as one of the authorized redirect urls in the provider’s console. That’s it! ![Bring your own Auth Credentials](/.netlify/images?url=_astro%2Fbyoc.DQgp0rVa.png\&w=1606\&h=1668\&dpl=69cce21a4f77360008b1503a) ## Migration from shared credentials [Section titled “Migration from shared credentials”](#migration-from-shared-credentials) If you’re currently using Scalekit’s shared credentials and want to migrate to BYOC: Note **Migration considerations:** * Users will need to re-authenticate with your OAuth applications * OAuth consent screens will change to show your branding * Rate limits and quotas will change to your application’s limits * Some users may need to re-grant permissions By implementing Bring Your Own Authentication, you gain complete control over your users’ authentication experience while maintaining the power and flexibility of Agent Auth’s unified API for tool execution. --- # DOCUMENT BOUNDARY --- # Domain Customization > Learn how to use a custom domain with Scalekit 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-xxxx.scalekit.com`. | Before | After | | ------------------------------ | -------------------------- | | `https://yourapp.scalekit.com` | `https://auth.yourapp.com` | * **Environment:** CNAME configuration is available only for production environments * **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) ![](/.netlify/images?url=_astro%2F1.BktW9U-H.png\&w=2786\&h=1746\&dpl=69cce21a4f77360008b1503a) 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 the Scalekit dashboard > Settings > Custom domains and paste it into the **Name/Label/Host** field in your DNS registrar 5. Copy the **Value** from the Scalekit dashboard > Settings > Custom domains 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/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 --- # Authorization - Overview > Learn about authorization options in Agent Auth, including OAuth flows, permissions, and security best practices. Agents that need to take actions on-behalf-of users in third party applications like gmail, calendar, slack, notion, hubspot etc need to do so in a secure, authorized manner. Scalekit’s Agent Auth solution helps developers build agents to act on-behalf-of users by managing user’s authentication and authorization for those tools. ## Supported Auth Methods [Section titled “Supported Auth Methods”](#supported-auth-methods) Agent Auth supports all the different types of authentication and authorization methods that are adopted by different applications so that you don’t have to worry about handling and managing user authorization tokens. * OAuth 2.0 * API Keys * Bearer Tokens * Custom JWTs ## Authorize a user [Section titled “Authorize a user”](#authorize-a-user) ### Create Connected Account [Section titled “Create Connected Account”](#create-connected-account) Create a connected\_account for a user and an application. In the example below - we show how to create a connected account for a user whose unique identifier is user\_123 and gmail application. ```python 1 # Create a connected account for user if it doesn't exist already 2 response = actions.get_or_create_connected_account( 3 connection_name="gmail", 4 identifier="user_123" 5 ) 6 connected_account = response.connected_account 7 print(f'Connected account created: {connected_account.id}') ``` ### Complete authorization [Section titled “Complete authorization”](#complete-authorization) Next, check the authorization status for this user’s connected account. If authorization status is not ACTIVE, generate a unique one-time magic link and redirect the user to this link. Depending on the application’s authentication type, Scalekit presents the user with appropriate next steps to complete user authorization. * If the application requires OAuth 2.0 based authorization, Scalekit will manage the OAuth 2.0 handshake on your behalf and keeps the user’s access token for subsequent tool calls. * If the application requires API Key based authentication, Scalekit will present them with a form to collect API Keys and other necessary information and stores them securely in an encrypted manner and uses them for subsequent tool calls. ```python 1 # If the user hasn't yet authorized the gmail connection or if the user's access token is expired, generate a link for them to authorize the connection 2 if(connected_account.status != "ACTIVE"): 3 print(f"gmail is not connected: {connected_account.status}") 4 link_response = actions.get_authorization_link( 5 connection_name="gmail", 6 identifier="user_123" 7 ) 8 print(f"🔗click on the link to authorize gmail", link_response.link) 9 10 # In a real app, redirect the user to this URL so that the user can complete the authentication process for their gmail account ``` ### Make Authorized Tool Calls [Section titled “Make Authorized Tool Calls”](#make-authorized-tool-calls) Once the user has successfully authorized the applications, your agent can use our SDK to execute tool calls on behalf of the user. Below is a small example to fetch user’s unread emails using the same connected account details. ```python 1 # Fetch recent emails 2 emails = actions.execute_tool( 3 connected_account_id=connected_account.id, 4 tool='gmail_fetch_mails', 5 tool_input={ 6 'query': 'is:unread', 7 'max_results': 5 8 } 9 ) 10 11 print(f'Recent emails: {emails.result}') ``` ## Next Steps [Section titled “Next Steps”](#next-steps) To make your agentic implementation faster, we have added Scalekit’s credentials for popular third party applications like GMail, Google Calendar, Google Drive etc. For a complete white-labelled experience, you can configure your own oauth credentials. [Bring your own Credentials ](/agent-auth/advanced/bring-your-own-oauth) --- # DOCUMENT BOUNDARY --- # Proxy API Calls > Use Scalekit managed authentication and make direct HTTP calls to third party applications Even though Scalekit Agent Auth offers pre-built tools and actions out of the box for the supported applications, if you would like to make direct API calls to the third party applications for any custom behaviour, you can leverage proxy\_api tool to directly invoke the third party application. Based on the connected account or user identifier details, Scalekit will automatically inject the user authorization tokens so that API calls to the third application will be successful. Proxy must be enabled per environment Proxy access for built-in providers (Gmail, Notion, Slack, and others) is **not enabled by default** on new environments. If you receive the error `proxy not enabled for provider`, contact to enable the proxy for your environment. ```python 1 # Fetch recent emails 2 emails = actions.tools.execute( 3 connected_account_id=connected_account.id, 4 tool='gmail_proxy_api', 5 parameters={ 6 'path': '/gmail/v1/users/me/messages', 7 'method': 'GET', 8 'headers': [{'Content-Type': 'application/json'}], 9 'params': [{'max_results': '5'}], 10 'body': '' #actual JSON payload 11 } 12 ) 13 14 print(f'Recent emails: {emails.result}') ``` As part of the above execution, Scalekit will automatically inject Bearer token in the request header before making the API call to GMAIL. --- # DOCUMENT BOUNDARY --- # Authentication Methods Comparison > Compare different authentication methods supported by Agent Auth including OAuth 2.0, API Keys, Bearer Tokens, and Custom JWT to choose the right approach. Agent Auth supports multiple authentication methods to connect with third-party providers. This guide helps you understand the differences and choose the right authentication method for your use case. ## Authentication methods overview [Section titled “Authentication methods overview”](#authentication-methods-overview) OAuth 2.0 **Most secure and widely supported** User-delegated authentication with automatic token refresh and granular permissions. **Best for:** Google, Microsoft, Slack, GitHub API Keys **Simple static credentials** Provider-issued keys for straightforward server-to-server authentication. **Best for:** Jira, Asana, Linear, Airtable Bearer Tokens **User-generated tokens** Personal access tokens with scoped permissions for individual use. **Best for:** GitHub PATs, GitLab tokens Custom JWT **Advanced signed tokens** Cryptographically signed tokens for service accounts and custom protocols. **Best for:** Custom integrations, service accounts ## Comparison matrix [Section titled “Comparison matrix”](#comparison-matrix) | Feature | OAuth 2.0 | API Keys | Bearer Tokens | Custom JWT | | ------------------------ | ---------- | -------- | ------------- | ------------ | | **Security Level** | High | Medium | Medium | High | | **User Interaction** | Required | Optional | Required | Not required | | **Token Refresh** | Automatic | N/A | Manual | Varies | | **Setup Complexity** | Moderate | Easy | Easy | Complex | | **Granular Permissions** | Yes | Limited | Yes | Limited | | **Provider Support** | Widespread | Common | Common | Limited | ## When to use each method [Section titled “When to use each method”](#when-to-use-each-method) ### OAuth 2.0 [Section titled “OAuth 2.0”](#oauth-20) **Use when:** * Provider supports OAuth * Acting on behalf of users * Need automatic token refresh * Require granular permissions * Building user-facing applications **Example:** User connects Gmail to send emails through your app ### API Keys [Section titled “API Keys”](#api-keys) **Use when:** * Provider only supports API keys * Building internal tools * Server-to-server communication * Simplicity is priority **Example:** Automated Jira ticket creation for support system ### Bearer Tokens [Section titled “Bearer Tokens”](#bearer-tokens) **Use when:** * Personal access is sufficient * Building developer tools * OAuth unavailable * User prefers manual control **Example:** Personal GitHub repository automation ### Custom JWT [Section titled “Custom JWT”](#custom-jwt) **Use when:** * Provider requires JWT * Service account access needed * Custom authentication protocol * Advanced security requirements **Example:** Enterprise service account integrations ## Next steps [Section titled “Next steps”](#next-steps) * [Providers](/agent-auth/providers) - Available third-party providers * [Connections](/agent-auth/connections) - Configure provider connections * [Authorization Methods](/agent-auth/tools/authorize) - Detailed authentication implementation --- # DOCUMENT BOUNDARY --- # Multi-Provider Authentication > Learn how to manage authentication for multiple third-party providers simultaneously, handle different auth states, and provide seamless user experiences. When building applications with Agent Auth, users often need to connect multiple third-party providers. This guide shows you how to manage multiple authenticated connections per user effectively. ## Understanding multi-provider scenarios [Section titled “Understanding multi-provider scenarios”](#understanding-multi-provider-scenarios) Users might connect multiple providers for different purposes: * **Email & Calendar**: Gmail + Google Calendar + Slack * **Project Management**: Jira + GitHub + Slack notifications * **Productivity Suite**: Microsoft 365 + Notion + Asana * **Support Systems**: Gmail + Slack + Jira + Salesforce ## Managing multiple connected accounts [Section titled “Managing multiple connected accounts”](#managing-multiple-connected-accounts) ### Create connections for multiple providers [Section titled “Create connections for multiple providers”](#create-connections-for-multiple-providers) Each provider requires a separate connected account: * Python ```python 1 # Create connected accounts for multiple providers 2 providers = ["gmail", "slack", "jira"] 3 user_id = "user_123" 4 5 for provider in providers: 6 response = actions.get_or_create_connected_account( 7 connection_name=provider, 8 identifier=user_id 9 ) 10 11 account = response.connected_account 12 print(f"{provider}: {account.status}") 13 14 # Generate authorization link if not active 15 if account.status != "ACTIVE": 16 link = actions.get_authorization_link( 17 connection_name=provider, 18 identifier=user_id 19 ) 20 print(f" Authorize {provider}: {link.link}") ``` * Node.js ```javascript 1 // Create connected accounts for multiple providers 2 const providers = ['gmail', 'slack', 'jira']; 3 const userId = 'user_123'; 4 5 for (const provider of providers) { 6 const response = await scalekit.actions.getOrCreateConnectedAccount({ 7 connectionName: provider, 8 identifier: userId 9 }); 10 11 const account = response.connectedAccount; 12 console.log(`${provider}: ${account.status}`); 13 14 // Generate authorization link if not active 15 if (account.status !== 'ACTIVE') { 16 const link = await scalekit.actions.getAuthorizationLink({ 17 connectionName: provider, 18 identifier: userId 19 }); 20 console.log(` Authorize ${provider}: ${link.link}`); 21 } 22 } ``` * Go ```go 1 // Create connected accounts for multiple providers 2 providers := []string{"gmail", "slack", "jira"} 3 userID := "user_123" 4 5 for _, provider := range providers { 6 response, err := scalekitClient.Actions.GetOrCreateConnectedAccount( 7 context.Background(), 8 provider, 9 userID, 10 ) 11 if err != nil { 12 log.Printf("Error for %s: %v", provider, err) 13 continue 14 } 15 16 account := response.ConnectedAccount 17 fmt.Printf("%s: %s\n", provider, account.Status) 18 19 // Generate authorization link if not active 20 if account.Status != "ACTIVE" { 21 link, _ := scalekitClient.Actions.GetAuthorizationLink( 22 context.Background(), 23 provider, 24 userID, 25 ) 26 fmt.Printf(" Authorize %s: %s\n", provider, link.Link) 27 } 28 } ``` * Java ```java 1 // Create connected accounts for multiple providers 2 String[] providers = {"gmail", "slack", "jira"}; 3 String userId = "user_123"; 4 5 for (String provider : providers) { 6 ConnectedAccountResponse response = scalekitClient.actions() 7 .getOrCreateConnectedAccount(provider, userId); 8 9 ConnectedAccount account = response.getConnectedAccount(); 10 System.out.println(provider + ": " + account.getStatus()); 11 12 // Generate authorization link if not active 13 if (!"ACTIVE".equals(account.getStatus())) { 14 AuthorizationLink link = scalekitClient.actions() 15 .getAuthorizationLink(provider, userId); 16 System.out.println(" Authorize " + provider + ": " + link.getLink()); 17 } 18 } ``` ### Check status across all providers [Section titled “Check status across all providers”](#check-status-across-all-providers) Monitor authentication status for all connected providers: * Python ```python 1 def get_user_connection_status(user_id: str, providers: list) -> dict: 2 """Get authentication status for all providers""" 3 status_map = {} 4 5 for provider in providers: 6 try: 7 account = actions.get_connected_account( 8 identifier=user_id, 9 connection_name=provider 10 ) 11 status_map[provider] = { 12 "status": account.status, 13 "last_updated": account.updated_at, 14 "scopes": account.scopes 15 } 16 except Exception as e: 17 status_map[provider] = { 18 "status": "NOT_CONNECTED", 19 "error": str(e) 20 } 21 22 return status_map 23 24 # Usage 25 providers = ["gmail", "slack", "jira", "github"] 26 status = get_user_connection_status("user_123", providers) 27 28 for provider, info in status.items(): 29 print(f"{provider}: {info['status']}") ``` * Node.js ```javascript 1 async function getUserConnectionStatus(userId, providers) { 2 /** 3 * Get authentication status for all providers 4 */ 5 const statusMap = {}; 6 7 for (const provider of providers) { 8 try { 9 const account = await scalekit.actions.getConnectedAccount({ 10 identifier: userId, 11 connectionName: provider 12 }); 13 14 statusMap[provider] = { 15 status: account.status, 16 lastUpdated: account.updatedAt, 17 scopes: account.scopes 18 }; 19 } catch (error) { 20 statusMap[provider] = { 21 status: 'NOT_CONNECTED', 22 error: error.message 23 }; 24 } 25 } 26 27 return statusMap; 28 } 29 30 // Usage 31 const providers = ['gmail', 'slack', 'jira', 'github']; 32 const status = await getUserConnectionStatus('user_123', providers); 33 34 Object.entries(status).forEach(([provider, info]) => { 35 console.log(`${provider}: ${info.status}`); 36 }); ``` * Go ```go 1 func GetUserConnectionStatus(userID string, providers []string) map[string]interface{} { 2 statusMap := make(map[string]interface{}) 3 4 for _, provider := range providers { 5 account, err := scalekitClient.Actions.GetConnectedAccount( 6 context.Background(), 7 userID, 8 provider, 9 ) 10 11 if err != nil { 12 statusMap[provider] = map[string]interface{}{ 13 "status": "NOT_CONNECTED", 14 "error": err.Error(), 15 } 16 } else { 17 statusMap[provider] = map[string]interface{}{ 18 "status": account.Status, 19 "lastUpdated": account.UpdatedAt, 20 "scopes": account.Scopes, 21 } 22 } 23 } 24 25 return statusMap 26 } ``` * Java ```java 1 public Map> getUserConnectionStatus( 2 String userId, List providers 3 ) { 4 Map> statusMap = new HashMap<>(); 5 6 for (String provider : providers) { 7 try { 8 ConnectedAccount account = scalekitClient.actions() 9 .getConnectedAccount(userId, provider); 10 11 Map info = new HashMap<>(); 12 info.put("status", account.getStatus()); 13 info.put("lastUpdated", account.getUpdatedAt()); 14 info.put("scopes", account.getScopes()); 15 statusMap.put(provider, info); 16 } catch (Exception e) { 17 Map info = new HashMap<>(); 18 info.put("status", "NOT_CONNECTED"); 19 info.put("error", e.getMessage()); 20 statusMap.put(provider, info); 21 } 22 } 23 24 return statusMap; 25 } ``` ## Handling different authentication states [Section titled “Handling different authentication states”](#handling-different-authentication-states) Different providers may have different states simultaneously: ```python 1 # Example: User's connection status 2 { 3 "gmail": "ACTIVE", # Working normally 4 "slack": "EXPIRED", # Needs token refresh 5 "jira": "PENDING", # User hasn't authorized yet 6 "github": "REVOKED" # User revoked access 7 } ``` ### Implement state-aware logic [Section titled “Implement state-aware logic”](#implement-state-aware-logic) ```python 1 def execute_multi_provider_workflow(user_id: str): 2 """ 3 Execute workflow requiring multiple providers. 4 Handle different authentication states gracefully. 5 """ 6 providers_status = { 7 "gmail": None, 8 "slack": None, 9 "jira": None 10 } 11 12 # Check status of all required providers 13 for provider in providers_status.keys(): 14 try: 15 account = actions.get_connected_account( 16 identifier=user_id, 17 connection_name=provider 18 ) 19 providers_status[provider] = account.status 20 except Exception: 21 providers_status[provider] = "NOT_CONNECTED" 22 23 # Determine what actions are possible 24 can_send_email = providers_status["gmail"] == "ACTIVE" 25 can_notify_slack = providers_status["slack"] == "ACTIVE" 26 can_create_ticket = providers_status["jira"] == "ACTIVE" 27 28 # Execute workflow with graceful degradation 29 results = {} 30 31 if can_send_email: 32 results["email"] = actions.execute_tool( 33 identifier=user_id, 34 tool_name="gmail_send_email", 35 tool_input={"to": "team@example.com", "subject": "Update"} 36 ) 37 else: 38 results["email"] = {"error": "Gmail not connected"} 39 40 if can_notify_slack: 41 results["slack"] = actions.execute_tool( 42 identifier=user_id, 43 tool_name="slack_send_message", 44 tool_input={"channel": "#general", "text": "Update posted"} 45 ) 46 else: 47 results["slack"] = {"error": "Slack not connected"} 48 49 if can_create_ticket: 50 results["jira"] = actions.execute_tool( 51 identifier=user_id, 52 tool_name="jira_create_issue", 53 tool_input={"project": "SUPPORT", "summary": "Customer inquiry"} 54 ) 55 else: 56 results["jira"] = {"error": "Jira not connected"} 57 58 # Report results to user 59 return { 60 "completed": [k for k, v in results.items() if "error" not in v], 61 "failed": [k for k, v in results.items() if "error" in v], 62 "details": results 63 } 64 65 # Usage 66 result = execute_multi_provider_workflow("user_123") 67 print(f"Completed: {result['completed']}") 68 print(f"Failed: {result['failed']}") ``` ## User experience patterns [Section titled “User experience patterns”](#user-experience-patterns) ### Connection management dashboard [Section titled “Connection management dashboard”](#connection-management-dashboard) Display all provider connections in user settings: ```python 1 def get_connection_dashboard_data(user_id: str) -> dict: 2 """Get data for user's connection management dashboard""" 3 supported_providers = ["gmail", "slack", "jira", "github", "calendar"] 4 5 dashboard_data = [] 6 7 for provider in supported_providers: 8 try: 9 account = actions.get_connected_account( 10 identifier=user_id, 11 connection_name=provider 12 ) 13 14 dashboard_data.append({ 15 "provider": provider, 16 "connected": True, 17 "status": account.status, 18 "last_updated": account.updated_at, 19 "can_reconnect": account.status in ["EXPIRED", "REVOKED"], 20 "reconnect_link": None if account.status == "ACTIVE" else 21 actions.get_authorization_link( 22 connection_name=provider, 23 identifier=user_id 24 ).link 25 }) 26 except Exception: 27 dashboard_data.append({ 28 "provider": provider, 29 "connected": False, 30 "status": "NOT_CONNECTED", 31 "connect_link": actions.get_authorization_link( 32 connection_name=provider, 33 identifier=user_id 34 ).link 35 }) 36 37 return { 38 "user_id": user_id, 39 "connections": dashboard_data, 40 "total_connected": sum(1 for c in dashboard_data if c["connected"]), 41 "needs_attention": sum( 42 1 for c in dashboard_data 43 if c.get("can_reconnect", False) 44 ) 45 } 46 47 # Usage - send this data to your frontend 48 dashboard = get_connection_dashboard_data("user_123") ``` ### Progressive connection onboarding [Section titled “Progressive connection onboarding”](#progressive-connection-onboarding) Guide users to connect providers as needed: ```python 1 def get_required_connections_for_feature(feature: str) -> list: 2 """Map features to required provider connections""" 3 feature_requirements = { 4 "email_automation": ["gmail"], 5 "team_notifications": ["slack"], 6 "project_sync": ["jira", "github"], 7 "calendar_scheduling": ["calendar"], 8 "full_productivity": ["gmail", "slack", "jira", "calendar", "github"] 9 } 10 11 return feature_requirements.get(feature, []) 12 13 def check_user_ready_for_feature(user_id: str, feature: str) -> dict: 14 """Check if user has connected all providers needed for feature""" 15 required_providers = get_required_connections_for_feature(feature) 16 17 connection_status = {} 18 missing_connections = [] 19 20 for provider in required_providers: 21 try: 22 account = actions.get_connected_account( 23 identifier=user_id, 24 connection_name=provider 25 ) 26 is_active = account.status == "ACTIVE" 27 connection_status[provider] = is_active 28 29 if not is_active: 30 missing_connections.append({ 31 "provider": provider, 32 "status": account.status, 33 "link": actions.get_authorization_link( 34 connection_name=provider, 35 identifier=user_id 36 ).link 37 }) 38 except Exception: 39 connection_status[provider] = False 40 missing_connections.append({ 41 "provider": provider, 42 "status": "NOT_CONNECTED", 43 "link": actions.get_authorization_link( 44 connection_name=provider, 45 identifier=user_id 46 ).link 47 }) 48 49 return { 50 "feature": feature, 51 "ready": len(missing_connections) == 0, 52 "connection_status": connection_status, 53 "missing_connections": missing_connections 54 } 55 56 # Usage 57 readiness = check_user_ready_for_feature("user_123", "email_automation") 58 if not readiness["ready"]: 59 print("Please connect the following providers:") 60 for conn in readiness["missing_connections"]: 61 print(f" - {conn['provider']}: {conn['link']}") ``` ## Bulk operations [Section titled “Bulk operations”](#bulk-operations) Execute operations across multiple providers efficiently: * Python ```python 1 def send_notification_to_all_channels(user_id: str, message: str): 2 """Send notification via all connected messaging platforms""" 3 messaging_providers = { 4 "slack": "slack_send_message", 5 "teams": "teams_send_message", 6 "discord": "discord_send_message" 7 } 8 9 results = {} 10 11 for provider, tool_name in messaging_providers.items(): 12 try: 13 # Check if provider is connected 14 account = actions.get_connected_account( 15 identifier=user_id, 16 connection_name=provider 17 ) 18 19 if account.status == "ACTIVE": 20 # Execute tool 21 result = actions.execute_tool( 22 identifier=user_id, 23 tool_name=tool_name, 24 tool_input={"text": message, "channel": "#notifications"} 25 ) 26 results[provider] = {"success": True, "result": result} 27 else: 28 results[provider] = { 29 "success": False, 30 "error": f"Not connected (status: {account.status})" 31 } 32 except Exception as e: 33 results[provider] = {"success": False, "error": str(e)} 34 35 return results 36 37 # Usage 38 notification_results = send_notification_to_all_channels( 39 "user_123", 40 "Deployment completed successfully!" 41 ) ``` * Node.js ```javascript 1 async function sendNotificationToAllChannels(userId, message) { 2 /** 3 * Send notification via all connected messaging platforms 4 */ 5 const messagingProviders = { 6 slack: 'slack_send_message', 7 teams: 'teams_send_message', 8 discord: 'discord_send_message' 9 }; 10 11 const results = {}; 12 13 for (const [provider, toolName] of Object.entries(messagingProviders)) { 14 try { 15 // Check if provider is connected 16 const account = await scalekit.actions.getConnectedAccount({ 17 identifier: userId, 18 connectionName: provider 19 }); 20 21 if (account.status === 'ACTIVE') { 22 // Execute tool 23 const result = await scalekit.actions.executeTool({ 24 identifier: userId, 25 toolName: toolName, 26 toolInput: { text: message, channel: '#notifications' } 27 }); 28 results[provider] = { success: true, result }; 29 } else { 30 results[provider] = { 31 success: false, 32 error: `Not connected (status: ${account.status})` 33 }; 34 } 35 } catch (error) { 36 results[provider] = { success: false, error: error.message }; 37 } 38 } 39 40 return results; 41 } ``` * Go ```go 1 func SendNotificationToAllChannels(userID, message string) map[string]interface{} { 2 messagingProviders := map[string]string{ 3 "slack": "slack_send_message", 4 "teams": "teams_send_message", 5 "discord": "discord_send_message", 6 } 7 8 results := make(map[string]interface{}) 9 10 for provider, toolName := range messagingProviders { 11 account, err := scalekitClient.Actions.GetConnectedAccount( 12 context.Background(), 13 userID, 14 provider, 15 ) 16 17 if err != nil { 18 results[provider] = map[string]interface{}{ 19 "success": false, 20 "error": err.Error(), 21 } 22 continue 23 } 24 25 if account.Status == "ACTIVE" { 26 result, err := scalekitClient.Actions.ExecuteTool( 27 context.Background(), 28 userID, 29 toolName, 30 map[string]interface{}{ 31 "text": message, 32 "channel": "#notifications", 33 }, 34 ) 35 36 if err != nil { 37 results[provider] = map[string]interface{}{ 38 "success": false, 39 "error": err.Error(), 40 } 41 } else { 42 results[provider] = map[string]interface{}{ 43 "success": true, 44 "result": result, 45 } 46 } 47 } 48 } 49 50 return results 51 } ``` * Java ```java 1 public Map> sendNotificationToAllChannels( 2 String userId, String message 3 ) { 4 Map messagingProviders = Map.of( 5 "slack", "slack_send_message", 6 "teams", "teams_send_message", 7 "discord", "discord_send_message" 8 ); 9 10 Map> results = new HashMap<>(); 11 12 for (Map.Entry entry : messagingProviders.entrySet()) { 13 String provider = entry.getKey(); 14 String toolName = entry.getValue(); 15 16 try { 17 ConnectedAccount account = scalekitClient.actions() 18 .getConnectedAccount(userId, provider); 19 20 if ("ACTIVE".equals(account.getStatus())) { 21 Map toolInput = Map.of( 22 "text", message, 23 "channel", "#notifications" 24 ); 25 26 ToolResult result = scalekitClient.actions() 27 .executeTool(userId, toolName, toolInput); 28 29 results.put(provider, Map.of("success", true, "result", result)); 30 } else { 31 results.put(provider, Map.of( 32 "success", false, 33 "error", "Not connected (status: " + account.getStatus() + ")" 34 )); 35 } 36 } catch (Exception e) { 37 results.put(provider, Map.of("success", false, "error", e.getMessage())); 38 } 39 } 40 41 return results; 42 } ``` ## Best practices [Section titled “Best practices”](#best-practices) ### Graceful degradation [Section titled “Graceful degradation”](#graceful-degradation) Design workflows that degrade gracefully when providers aren’t connected: ```python 1 # Good: Workflow continues with available providers 2 if gmail_connected: 3 send_email() 4 if slack_connected: 5 notify_slack() 6 # User gets partial functionality 7 8 # Bad: Workflow fails completely 9 if not (gmail_connected and slack_connected): 10 raise Error("Connect all providers first") ``` ### Clear status communication [Section titled “Clear status communication”](#clear-status-communication) Show users which providers are connected and which need attention: ```python 1 dashboard_message = f""" 2 Your Connections: 3 ✓ Gmail: Connected and working 4 ⚠ Slack: Token expired - reconnect now 5 ✗ Jira: Not connected - connect to enable tickets 6 ✓ Calendar: Connected and working 7 """ ``` ### Proactive reconnection prompts [Section titled “Proactive reconnection prompts”](#proactive-reconnection-prompts) Notify users before connections become critical: ```python 1 def check_and_notify_expiring_connections(user_id: str): 2 """Check for connections that need attention""" 3 providers = ["gmail", "slack", "jira", "calendar"] 4 5 needs_attention = [] 6 7 for provider in providers: 8 try: 9 account = actions.get_connected_account( 10 identifier=user_id, 11 connection_name=provider 12 ) 13 14 if account.status in ["EXPIRED", "REVOKED"]: 15 needs_attention.append({ 16 "provider": provider, 17 "status": account.status, 18 "reconnect_link": actions.get_authorization_link( 19 connection_name=provider, 20 identifier=user_id 21 ).link 22 }) 23 except Exception: 24 continue 25 26 if needs_attention: 27 # Send notification to user 28 print(f"⚠ {len(needs_attention)} connection(s) need your attention") 29 for conn in needs_attention: 30 print(f" - {conn['provider']}: {conn['status']}") 31 32 return needs_attention ``` ## Next steps [Section titled “Next steps”](#next-steps) * [Testing Authentication](/agent-auth/authentication/testing-auth-flows) - Testing multi-provider scenarios * [Troubleshooting](/agent-auth/authentication/troubleshooting) - Debugging multi-provider issues --- # DOCUMENT BOUNDARY --- # Scopes and Permissions > Learn how to manage OAuth scopes and permissions for Agent Auth connections to control what your application can access. OAuth scopes and permissions determine what data and actions your application can access on behalf of users. Understanding how to properly configure and manage scopes is essential for building secure and functional Agent Auth integrations. ## Understanding OAuth scopes [Section titled “Understanding OAuth scopes”](#understanding-oauth-scopes) OAuth scopes are permission grants that define the level of access your application has to a user’s data with third-party providers. ### What are scopes? [Section titled “What are scopes?”](#what-are-scopes) Scopes are strings that represent specific permissions: ```plaintext 1 # Example OAuth scopes 2 https://www.googleapis.com/auth/gmail.readonly # Read Gmail messages 3 https://www.googleapis.com/auth/gmail.send # Send Gmail messages 4 https://www.googleapis.com/auth/calendar.events # Manage calendar events 5 channels:read # Read Slack channels 6 chat:write # Send Slack messages ``` \###How scopes work 1. **Application requests scopes** - Your connection specifies required scopes 2. **User sees consent screen** - Provider shows what permissions are requested 3. **User grants access** - User approves or denies the requested permissions 4. **Tokens include scopes** - Access tokens are limited to granted scopes 5. **API enforces scopes** - Provider APIs check tokens have required scopes ### Scope granularity [Section titled “Scope granularity”](#scope-granularity) Scopes typically follow a hierarchy from broad to specific: **Gmail example:** * `https://mail.google.com/` - Full Gmail access (read, send, delete) * `https://www.googleapis.com/auth/gmail.modify` - Read and modify (but not delete) * `https://www.googleapis.com/auth/gmail.readonly` - Read-only access * `https://www.googleapis.com/auth/gmail.send` - Send emails only Principle of least privilege Always request the minimum scopes necessary for your application’s functionality. Users are more likely to grant limited, specific permissions than broad access. ## Provider-specific scopes [Section titled “Provider-specific scopes”](#provider-specific-scopes) Different providers use different scope formats and naming conventions: ### Google Workspace scopes [Section titled “Google Workspace scopes”](#google-workspace-scopes) Google uses URL-based scopes with hierarchical permissions: Gmail Scopes **Read-only access:** ```plaintext 1 https://www.googleapis.com/auth/gmail.readonly ``` **Send emails:** ```plaintext 1 https://www.googleapis.com/auth/gmail.send ``` **Full access:** ```plaintext 1 https://mail.google.com/ ``` **Modify (read/write, no delete):** ```plaintext 1 https://www.googleapis.com/auth/gmail.modify ``` Google Calendar Scopes **Read-only calendar access:** ```plaintext 1 https://www.googleapis.com/auth/calendar.readonly ``` **Manage calendar events:** ```plaintext 1 https://www.googleapis.com/auth/calendar.events ``` **Full calendar access:** ```plaintext 1 https://www.googleapis.com/auth/calendar ``` Google Drive Scopes **Read-only access:** ```plaintext 1 https://www.googleapis.com/auth/drive.readonly ``` **Per-file access:** ```plaintext 1 https://www.googleapis.com/auth/drive.file ``` **Full drive access:** ```plaintext 1 https://www.googleapis.com/auth/drive ``` Google Sheets Scopes **Read-only sheets:** ```plaintext 1 https://www.googleapis.com/auth/spreadsheets.readonly ``` **Edit sheets:** ```plaintext 1 https://www.googleapis.com/auth/spreadsheets ``` ### Microsoft 365 scopes [Section titled “Microsoft 365 scopes”](#microsoft-365-scopes) Microsoft uses dotted notation with resource.permission format: Outlook/Mail Scopes **Read mail:** ```plaintext 1 Mail.Read ``` **Send mail:** ```plaintext 1 Mail.Send ``` **Read/write mail:** ```plaintext 1 Mail.ReadWrite ``` Calendar Scopes **Read calendar:** ```plaintext 1 Calendars.Read ``` **Manage calendar:** ```plaintext 1 Calendars.ReadWrite ``` OneDrive Scopes **Read files:** ```plaintext 1 Files.Read.All ``` **Read/write files:** ```plaintext 1 Files.ReadWrite.All ``` Teams Scopes **Read teams:** ```plaintext 1 Team.ReadBasic.All ``` **Send messages:** ```plaintext 1 ChannelMessage.Send ``` ### Slack scopes [Section titled “Slack scopes”](#slack-scopes) Slack uses simple string-based scopes: Channel Scopes **Read channels:** ```plaintext 1 channels:read ``` **Manage channels:** ```plaintext 1 channels:manage ``` **Join channels:** ```plaintext 1 channels:join ``` Chat Scopes **Send messages:** ```plaintext 1 chat:write ``` **Send as user:** ```plaintext 1 chat:write.customize ``` User Scopes **Read user info:** ```plaintext 1 users:read ``` **Read user email:** ```plaintext 1 users:read.email ``` File Scopes **Read files:** ```plaintext 1 files:read ``` **Write files:** ```plaintext 1 files:write ``` ### Jira/Atlassian scopes [Section titled “Jira/Atlassian scopes”](#jiraatlassian-scopes) Atlassian uses colon-separated scopes: ```plaintext 1 read:jira-work # Read issues and projects 2 write:jira-work # Create and update issues 3 read:jira-user # Read user information 4 manage:jira-project # Manage projects ``` ## Configuring scopes in connections [Section titled “Configuring scopes in connections”](#configuring-scopes-in-connections) Scopes are configured at the connection level in Scalekit: ### Using Scalekit dashboard [Section titled “Using Scalekit dashboard”](#using-scalekit-dashboard) 1. Navigate to **Agent Auth** → **Connections** 2. Select your connection or create a new one 3. In the **Scopes** section, enter required scopes 4. Scopes vary by provider - refer to provider’s documentation 5. Save the connection configuration 6. Existing users must re-authenticate to get new scopes ### Scope configuration examples [Section titled “Scope configuration examples”](#scope-configuration-examples) **Gmail connection with multiple scopes:** ```javascript 1 // Dashboard configuration (for reference) 2 { 3 "connection_name": "gmail", 4 "provider": "GMAIL", 5 "scopes": [ 6 "https://www.googleapis.com/auth/gmail.readonly", 7 "https://www.googleapis.com/auth/gmail.send", 8 "https://www.googleapis.com/auth/gmail.modify" 9 ] 10 } ``` **Slack connection with workspace scopes:** ```javascript 1 // Dashboard configuration (for reference) 2 { 3 "connection_name": "slack", 4 "provider": "SLACK", 5 "scopes": [ 6 "channels:read", 7 "chat:write", 8 "users:read", 9 "files:read" 10 ] 11 } ``` ## Checking granted scopes [Section titled “Checking granted scopes”](#checking-granted-scopes) Verify which scopes a user has granted: * Python ```python 1 # Get connected account and check granted scopes 2 account = actions.get_connected_account( 3 identifier="user_123", 4 connection_name="gmail" 5 ) 6 7 print(f"Granted scopes: {account.scopes}") 8 9 # Check if specific scope is granted 10 required_scope = "https://www.googleapis.com/auth/gmail.send" 11 if required_scope in account.scopes: 12 print("✓ User granted email sending permission") 13 else: 14 print("✗ Email sending permission not granted") 15 # Request re-authentication with required scope ``` * Node.js ```javascript 1 // Get connected account and check granted scopes 2 const account = await scalekit.actions.getConnectedAccount({ 3 identifier: 'user_123', 4 connectionName: 'gmail' 5 }); 6 7 console.log(`Granted scopes: ${account.scopes}`); 8 9 // Check if specific scope is granted 10 const requiredScope = 'https://www.googleapis.com/auth/gmail.send'; 11 if (account.scopes.includes(requiredScope)) { 12 console.log('✓ User granted email sending permission'); 13 } else { 14 console.log('✗ Email sending permission not granted'); 15 // Request re-authentication with required scope 16 } ``` * Go ```go 1 // Get connected account and check granted scopes 2 account, err := scalekitClient.Actions.GetConnectedAccount( 3 context.Background(), 4 "user_123", 5 "gmail", 6 ) 7 if err != nil { 8 log.Fatal(err) 9 } 10 11 fmt.Printf("Granted scopes: %v\n", account.Scopes) 12 13 // Check if specific scope is granted 14 requiredScope := "https://www.googleapis.com/auth/gmail.send" 15 hasScope := false 16 for _, scope := range account.Scopes { 17 if scope == requiredScope { 18 hasScope = true 19 break 20 } 21 } 22 23 if hasScope { 24 fmt.Println("✓ User granted email sending permission") 25 } else { 26 fmt.Println("✗ Email sending permission not granted") 27 } ``` * Java ```java 1 // Get connected account and check granted scopes 2 ConnectedAccount account = scalekitClient.actions().getConnectedAccount( 3 "user_123", 4 "gmail" 5 ); 6 7 System.out.println("Granted scopes: " + account.getScopes()); 8 9 // Check if specific scope is granted 10 String requiredScope = "https://www.googleapis.com/auth/gmail.send"; 11 if (account.getScopes().contains(requiredScope)) { 12 System.out.println("✓ User granted email sending permission"); 13 } else { 14 System.out.println("✗ Email sending permission not granted"); 15 // Request re-authentication with required scope 16 } ``` ## Requesting additional scopes [Section titled “Requesting additional scopes”](#requesting-additional-scopes) When you need additional permissions, users must re-authenticate: ### Scope upgrade flow [Section titled “Scope upgrade flow”](#scope-upgrade-flow) 1. **Update connection** - Add new scopes to connection configuration 2. **Detect missing scopes** - Check connected account for required scopes 3. **Generate auth link** - Create new authorization link for user 4. **User re-authenticates** - User approves additional permissions 5. **Verify new scopes** - Confirm scopes were granted ### Implementation example [Section titled “Implementation example”](#implementation-example) * Python ```python 1 def ensure_required_scopes(identifier: str, connection_name: str, required_scopes: list): 2 """ 3 Ensure user has granted all required scopes. 4 Returns True if all scopes granted, False if re-authentication needed. 5 """ 6 # Get current account and scopes 7 account = actions.get_connected_account( 8 identifier=identifier, 9 connection_name=connection_name 10 ) 11 12 # Check if all required scopes are granted 13 granted_scopes = set(account.scopes) 14 missing_scopes = [s for s in required_scopes if s not in granted_scopes] 15 16 if not missing_scopes: 17 print("✓ All required scopes granted") 18 return True 19 20 print(f"⚠ Missing scopes: {missing_scopes}") 21 22 # Generate authorization link for re-authentication 23 link_response = actions.get_authorization_link( 24 connection_name=connection_name, 25 identifier=identifier 26 ) 27 28 print(f"🔗 User must re-authorize with additional permissions:") 29 print(f" {link_response.link}") 30 print(f"\nMissing permissions:") 31 for scope in missing_scopes: 32 print(f" - {scope}") 33 34 return False 35 36 # Usage 37 required_scopes = [ 38 "https://www.googleapis.com/auth/gmail.readonly", 39 "https://www.googleapis.com/auth/gmail.send", 40 "https://www.googleapis.com/auth/gmail.modify" 41 ] 42 43 if ensure_required_scopes("user_123", "gmail", required_scopes): 44 # All scopes granted, proceed with operation 45 result = actions.execute_tool(...) 46 else: 47 # Waiting for user to re-authenticate 48 print("Please authorize additional permissions") ``` * Node.js ```javascript 1 async function ensureRequiredScopes(identifier, connectionName, requiredScopes) { 2 /** 3 * Ensure user has granted all required scopes. 4 * Returns true if all scopes granted, false if re-authentication needed. 5 */ 6 // Get current account and scopes 7 const account = await scalekit.actions.getConnectedAccount({ 8 identifier, 9 connectionName 10 }); 11 12 // Check if all required scopes are granted 13 const grantedScopes = new Set(account.scopes); 14 const missingScopes = requiredScopes.filter(s => !grantedScopes.has(s)); 15 16 if (missingScopes.length === 0) { 17 console.log('✓ All required scopes granted'); 18 return true; 19 } 20 21 console.log(`⚠ Missing scopes: ${missingScopes.join(', ')}`); 22 23 // Generate authorization link for re-authentication 24 const linkResponse = await scalekit.actions.getAuthorizationLink({ 25 connectionName, 26 identifier 27 }); 28 29 console.log('🔗 User must re-authorize with additional permissions:'); 30 console.log(` ${linkResponse.link}`); 31 console.log('\nMissing permissions:'); 32 missingScopes.forEach(scope => console.log(` - ${scope}`)); 33 34 return false; 35 } 36 37 // Usage 38 const requiredScopes = [ 39 'https://www.googleapis.com/auth/gmail.readonly', 40 'https://www.googleapis.com/auth/gmail.send', 41 'https://www.googleapis.com/auth/gmail.modify' 42 ]; 43 44 if (await ensureRequiredScopes('user_123', 'gmail', requiredScopes)) { 45 // All scopes granted, proceed with operation 46 const result = await scalekit.actions.executeTool(...); 47 } else { 48 // Waiting for user to re-authenticate 49 console.log('Please authorize additional permissions'); 50 } ``` * Go ```go 1 func ensureRequiredScopes(identifier, connectionName string, requiredScopes []string) (bool, error) { 2 // Get current account and scopes 3 account, err := scalekitClient.Actions.GetConnectedAccount( 4 context.Background(), 5 identifier, 6 connectionName, 7 ) 8 if err != nil { 9 return false, err 10 } 11 12 // Check if all required scopes are granted 13 grantedScopes := make(map[string]bool) 14 for _, scope := range account.Scopes { 15 grantedScopes[scope] = true 16 } 17 18 var missingScopes []string 19 for _, scope := range requiredScopes { 20 if !grantedScopes[scope] { 21 missingScopes = append(missingScopes, scope) 22 } 23 } 24 25 if len(missingScopes) == 0 { 26 fmt.Println("✓ All required scopes granted") 27 return true, nil 28 } 29 30 fmt.Printf("⚠ Missing scopes: %v\n", missingScopes) 31 32 // Generate authorization link 33 linkResponse, err := scalekitClient.Actions.GetAuthorizationLink( 34 context.Background(), 35 connectionName, 36 identifier, 37 ) 38 if err != nil { 39 return false, err 40 } 41 42 fmt.Printf("🔗 User must re-authorize: %s\n", linkResponse.Link) 43 44 return false, nil 45 } ``` * Java ```java 1 public boolean ensureRequiredScopes(String identifier, String connectionName, List requiredScopes) { 2 try { 3 // Get current account and scopes 4 ConnectedAccount account = scalekitClient.actions().getConnectedAccount( 5 identifier, 6 connectionName 7 ); 8 9 // Check if all required scopes are granted 10 Set grantedScopes = new HashSet<>(account.getScopes()); 11 List missingScopes = requiredScopes.stream() 12 .filter(s -> !grantedScopes.contains(s)) 13 .collect(Collectors.toList()); 14 15 if (missingScopes.isEmpty()) { 16 System.out.println("✓ All required scopes granted"); 17 return true; 18 } 19 20 System.out.println("⚠ Missing scopes: " + String.join(", ", missingScopes)); 21 22 // Generate authorization link 23 AuthorizationLink linkResponse = scalekitClient.actions().getAuthorizationLink( 24 connectionName, 25 identifier 26 ); 27 28 System.out.println("🔗 User must re-authorize: " + linkResponse.getLink()); 29 System.out.println("\nMissing permissions:"); 30 missingScopes.forEach(scope -> System.out.println(" - " + scope)); 31 32 return false; 33 } catch (Exception e) { 34 System.err.println("Error checking scopes: " + e.getMessage()); 35 return false; 36 } 37 } ``` ## Scope validation before tool execution [Section titled “Scope validation before tool execution”](#scope-validation-before-tool-execution) Always validate scopes before executing tools to provide better error messages: ```python 1 # Map tools to required scopes 2 TOOL_SCOPE_REQUIREMENTS = { 3 'gmail_send_email': ['https://www.googleapis.com/auth/gmail.send'], 4 'gmail_fetch_mails': ['https://www.googleapis.com/auth/gmail.readonly'], 5 'gmail_delete_email': ['https://mail.google.com/'], 6 'calendar_create_event': ['https://www.googleapis.com/auth/calendar.events'], 7 'slack_send_message': ['chat:write'], 8 } 9 10 def execute_tool_with_scope_check(identifier, connection_name, tool_name, tool_input): 11 """Execute tool after validating required scopes""" 12 # Get required scopes for this tool 13 required_scopes = TOOL_SCOPE_REQUIREMENTS.get(tool_name, []) 14 15 if required_scopes: 16 # Verify user has granted required scopes 17 account = actions.get_connected_account( 18 identifier=identifier, 19 connection_name=connection_name 20 ) 21 22 granted_scopes = set(account.scopes) 23 missing_scopes = [s for s in required_scopes if s not in granted_scopes] 24 25 if missing_scopes: 26 raise PermissionError( 27 f"Missing required permissions for {tool_name}: {missing_scopes}. " 28 f"Please re-authorize to grant these permissions." 29 ) 30 31 # Scopes verified, execute tool 32 return actions.execute_tool( 33 identifier=identifier, 34 tool_name=tool_name, 35 tool_input=tool_input 36 ) 37 38 # Usage 39 try: 40 result = execute_tool_with_scope_check( 41 identifier="user_123", 42 connection_name="gmail", 43 tool_name="gmail_send_email", 44 tool_input={"to": "user@example.com", "subject": "Test", "body": "Hello"} 45 ) 46 print("✓ Email sent successfully") 47 except PermissionError as e: 48 print(f"✗ Permission error: {e}") 49 # Prompt user to re-authorize ``` ## Best practices [Section titled “Best practices”](#best-practices) ### Request minimum necessary scopes [Section titled “Request minimum necessary scopes”](#request-minimum-necessary-scopes) Tip Only request scopes your application actually uses. Users are more likely to grant limited, specific permissions. **Good:** ```python 1 # Only request scopes you need 2 scopes = [ 3 "https://www.googleapis.com/auth/gmail.readonly", # For reading emails 4 "https://www.googleapis.com/auth/gmail.send" # For sending emails 5 ] ``` **Avoid:** ```python 1 # Don't request overly broad access 2 scopes = [ 3 "https://mail.google.com/" # Full Gmail access including delete 4 ] ``` ### Explain permissions to users [Section titled “Explain permissions to users”](#explain-permissions-to-users) Provide clear explanations of why you need specific permissions: ```python 1 SCOPE_EXPLANATIONS = { 2 "https://www.googleapis.com/auth/gmail.readonly": 3 "Read your emails to analyze and summarize them", 4 "https://www.googleapis.com/auth/gmail.send": 5 "Send emails on your behalf", 6 "https://www.googleapis.com/auth/calendar.events": 7 "Create and manage calendar events for you", 8 "chat:write": 9 "Send messages in Slack channels", 10 } 11 12 # Show explanations in your UI before redirecting to OAuth 13 def get_scope_explanation(scope): 14 return SCOPE_EXPLANATIONS.get(scope, "Access your account data") ``` ### Handle scope denials gracefully [Section titled “Handle scope denials gracefully”](#handle-scope-denials-gracefully) ```python 1 # After OAuth callback 2 if user_denied_scopes: 3 # Don't show error - explain what features won't work 4 message = """ 5 Some features will be limited because certain permissions weren't granted: 6 - Email sending: Requires 'Send email' permission 7 - Email reading: Requires 'Read email' permission 8 9 You can grant these permissions later in Settings. 10 """ 11 # Provide link to re-authorize in settings ``` ### Incremental authorization [Section titled “Incremental authorization”](#incremental-authorization) Request additional scopes only when needed: ```python 1 # Start with minimal scopes 2 initial_scopes = ["https://www.googleapis.com/auth/gmail.readonly"] 3 4 # Later, when user wants to send email 5 if user_wants_to_send_email: 6 # Request additional scope 7 additional_scopes = ["https://www.googleapis.com/auth/gmail.send"] 8 # Prompt user to grant additional permission ``` ## Troubleshooting scope issues [Section titled “Troubleshooting scope issues”](#troubleshooting-scope-issues) ### Insufficient permissions error [Section titled “Insufficient permissions error”](#insufficient-permissions-error) **Error:** “Insufficient permissions” or 403 Forbidden **Solution:** 1. Check which scopes are currently granted 2. Verify the tool requires those specific scopes 3. Update connection configuration if needed 4. Have user re-authenticate to grant additional scopes ### Scope not available for provider [Section titled “Scope not available for provider”](#scope-not-available-for-provider) **Error:** Invalid scope or scope not recognized **Solution:** 1. Verify scope name matches provider’s documentation exactly 2. Check if scope requires special provider approval 3. Some scopes only available to verified applications 4. Review provider’s scope documentation for correct format ### User sees unexpected consent screen [Section titled “User sees unexpected consent screen”](#user-sees-unexpected-consent-screen) **Issue:** OAuth consent shows different or additional permissions **Causes:** * Scopes configured in connection don’t match expected * Provider groups related scopes together * Sensitive scopes trigger additional consent **Solution:** * Review connection scope configuration * Check provider’s scope grouping behavior * Ensure sensitive scopes are truly necessary ## Next steps [Section titled “Next steps”](#next-steps) * [Authentication Troubleshooting](/agent-auth/authentication/troubleshooting) - Debugging auth issues * [Multi-Provider Authentication](/agent-auth/authentication/multi-provider) - Managing multiple provider connections --- # DOCUMENT BOUNDARY --- # Testing Authentication Flows > Learn how to test Agent Auth authentication flows in development, staging, and production environments with comprehensive testing strategies. Thorough testing of authentication flows ensures your Agent Auth integration works reliably before production deployment. This guide covers testing strategies, tools, and best practices. ## Testing environments [Section titled “Testing environments”](#testing-environments) ### Development environment [Section titled “Development environment”](#development-environment) **Purpose:** Rapid iteration and debugging **Characteristics:** * Local development server * Test accounts and credentials * Verbose logging enabled * Quick feedback loops **Setup:** development.env ```python 1 SCALEKIT_ENV_URL=https://your-env.scalekit.dev 2 SCALEKIT_CLIENT_ID=dev_client_id 3 SCALEKIT_CLIENT_SECRET=dev_client_secret 4 DEBUG=true 5 LOG_LEVEL=debug ``` ### Staging environment [Section titled “Staging environment”](#staging-environment) **Purpose:** Pre-production validation **Characteristics:** * Production-like configuration * Realistic data volumes * Integration with staging third-party accounts * Performance testing **Setup:** staging.env ```python 1 SCALEKIT_ENV_URL=https://your-env.scalekit.cloud 2 SCALEKIT_CLIENT_ID=staging_client_id 3 SCALEKIT_CLIENT_SECRET=staging_client_secret 4 DEBUG=false 5 LOG_LEVEL=info ``` ### Production environment [Section titled “Production environment”](#production-environment) **Purpose:** Live user traffic **Characteristics:** * Real user data * Verified OAuth applications * Monitoring and alerts * Minimal logging **Setup:** production.env ```python 1 SCALEKIT_ENV_URL=https://your-env.scalekit.cloud 2 SCALEKIT_CLIENT_ID=prod_client_id 3 SCALEKIT_CLIENT_SECRET=prod_client_secret 4 DEBUG=false 5 LOG_LEVEL=warn ``` ## Test account setup [Section titled “Test account setup”](#test-account-setup) ### Creating test providers [Section titled “Creating test providers”](#creating-test-providers) Set up test accounts for each provider: **Google Workspace:** 1. Create test Google account 2. Enable 2FA if testing MFA scenarios 3. Use for Gmail, Calendar, Drive testing **Slack:** 1. Create free Slack workspace 2. Install your Slack app 3. Use for messaging and notification testing **Microsoft 365:** 1. Get Microsoft 365 developer account (free) 2. Create test users 3. Use for Outlook, Teams, OneDrive testing **Jira/Atlassian:** 1. Create free Atlassian Cloud account 2. Set up test projects 3. Generate API tokens for testing ### Test user patterns [Section titled “Test user patterns”](#test-user-patterns) Create different test users for scenarios: ```python 1 # Test user configurations 2 TEST_USERS = { 3 "basic_user": { 4 "identifier": "test_user_001", 5 "providers": ["gmail"], 6 "scenario": "Single provider, basic authentication" 7 }, 8 "power_user": { 9 "identifier": "test_user_002", 10 "providers": ["gmail", "slack", "jira", "calendar"], 11 "scenario": "Multiple providers, full feature access" 12 }, 13 "expired_user": { 14 "identifier": "test_user_003", 15 "providers": ["gmail"], 16 "scenario": "Expired tokens, test refresh logic", 17 "setup": "Manually expire tokens" 18 }, 19 "revoked_user": { 20 "identifier": "test_user_004", 21 "providers": ["slack"], 22 "scenario": "User revoked access, test re-auth flow" 23 } 24 } ``` ## Unit testing authentication [Section titled “Unit testing authentication”](#unit-testing-authentication) ### Test connected account creation [Section titled “Test connected account creation”](#test-connected-account-creation) * Python ```python 1 import unittest 2 from unittest.mock import Mock, patch 3 4 class TestConnectedAccountCreation(unittest.TestCase): 5 def setUp(self): 6 self.actions = Mock() 7 self.user_id = "test_user_123" 8 self.provider = "gmail" 9 10 def test_create_connected_account_success(self): 11 """Test successful connected account creation""" 12 # Mock response 13 mock_response = Mock() 14 mock_response.connected_account = Mock( 15 id="account_123", 16 status="PENDING", 17 connection_name="gmail" 18 ) 19 self.actions.get_or_create_connected_account.return_value = mock_response 20 21 # Execute 22 response = self.actions.get_or_create_connected_account( 23 connection_name=self.provider, 24 identifier=self.user_id 25 ) 26 27 # Assert 28 self.assertEqual(response.connected_account.status, "PENDING") 29 self.assertEqual(response.connected_account.connection_name, "gmail") 30 31 def test_generate_authorization_link(self): 32 """Test authorization link generation""" 33 mock_response = Mock() 34 mock_response.link = "https://accounts.google.com/oauth/authorize?..." 35 36 self.actions.get_authorization_link.return_value = mock_response 37 38 response = self.actions.get_authorization_link( 39 connection_name=self.provider, 40 identifier=self.user_id 41 ) 42 43 self.assertIn("https://", response.link) 44 self.actions.get_authorization_link.assert_called_once() 45 46 if __name__ == '__main__': 47 unittest.main() ``` * Node.js ```javascript 1 const { describe, it, expect, jest, beforeEach } = require('@jest/globals'); 2 3 describe('Connected Account Creation', () => { 4 let mockActions; 5 const userId = 'test_user_123'; 6 const provider = 'gmail'; 7 8 beforeEach(() => { 9 mockActions = { 10 getOrCreateConnectedAccount: jest.fn(), 11 getAuthorizationLink: jest.fn() 12 }; 13 }); 14 15 it('should create connected account successfully', async () => { 16 // Mock response 17 const mockResponse = { 18 connectedAccount: { 19 id: 'account_123', 20 status: 'PENDING', 21 connectionName: 'gmail' 22 } 23 }; 24 25 mockActions.getOrCreateConnectedAccount.mockResolvedValue(mockResponse); 26 27 // Execute 28 const response = await mockActions.getOrCreateConnectedAccount({ 29 connectionName: provider, 30 identifier: userId 31 }); 32 33 // Assert 34 expect(response.connectedAccount.status).toBe('PENDING'); 35 expect(response.connectedAccount.connectionName).toBe('gmail'); 36 }); 37 38 it('should generate authorization link', async () => { 39 const mockResponse = { 40 link: 'https://accounts.google.com/oauth/authorize?...' 41 }; 42 43 mockActions.getAuthorizationLink.mockResolvedValue(mockResponse); 44 45 const response = await mockActions.getAuthorizationLink({ 46 connectionName: provider, 47 identifier: userId 48 }); 49 50 expect(response.link).toContain('https://'); 51 expect(mockActions.getAuthorizationLink).toHaveBeenCalledTimes(1); 52 }); 53 }); ``` * Go ```go 1 package auth_test 2 3 import ( 4 "testing" 5 "github.com/stretchr/testify/assert" 6 "github.com/stretchr/testify/mock" 7 ) 8 9 type MockActions struct { 10 mock.Mock 11 } 12 13 func (m *MockActions) GetOrCreateConnectedAccount(connectionName, identifier string) (*ConnectedAccountResponse, error) { 14 args := m.Called(connectionName, identifier) 15 return args.Get(0).(*ConnectedAccountResponse), args.Error(1) 16 } 17 18 func TestCreateConnectedAccount(t *testing.T) { 19 // Arrange 20 mockActions := new(MockActions) 21 userId := "test_user_123" 22 provider := "gmail" 23 24 expectedResponse := &ConnectedAccountResponse{ 25 ConnectedAccount: ConnectedAccount{ 26 ID: "account_123", 27 Status: "PENDING", 28 ConnectionName: "gmail", 29 }, 30 } 31 32 mockActions.On("GetOrCreateConnectedAccount", provider, userId). 33 Return(expectedResponse, nil) 34 35 // Act 36 response, err := mockActions.GetOrCreateConnectedAccount(provider, userId) 37 38 // Assert 39 assert.NoError(t, err) 40 assert.Equal(t, "PENDING", response.ConnectedAccount.Status) 41 assert.Equal(t, "gmail", response.ConnectedAccount.ConnectionName) 42 mockActions.AssertExpectations(t) 43 } ``` * Java ```java 1 import org.junit.jupiter.api.BeforeEach; 2 import org.junit.jupiter.api.Test; 3 import org.mockito.Mock; 4 import org.mockito.MockitoAnnotations; 5 import static org.junit.jupiter.api.Assertions.*; 6 import static org.mockito.Mockito.*; 7 8 class ConnectedAccountCreationTest { 9 @Mock 10 private Actions mockActions; 11 12 private String userId; 13 private String provider; 14 15 @BeforeEach 16 void setUp() { 17 MockitoAnnotations.openMocks(this); 18 userId = "test_user_123"; 19 provider = "gmail"; 20 } 21 22 @Test 23 void testCreateConnectedAccountSuccess() { 24 // Arrange 25 ConnectedAccount account = new ConnectedAccount(); 26 account.setId("account_123"); 27 account.setStatus("PENDING"); 28 account.setConnectionName("gmail"); 29 30 ConnectedAccountResponse mockResponse = new ConnectedAccountResponse(); 31 mockResponse.setConnectedAccount(account); 32 33 when(mockActions.getOrCreateConnectedAccount(provider, userId)) 34 .thenReturn(mockResponse); 35 36 // Act 37 ConnectedAccountResponse response = mockActions 38 .getOrCreateConnectedAccount(provider, userId); 39 40 // Assert 41 assertEquals("PENDING", response.getConnectedAccount().getStatus()); 42 assertEquals("gmail", response.getConnectedAccount().getConnectionName()); 43 verify(mockActions, times(1)).getOrCreateConnectedAccount(provider, userId); 44 } 45 } ``` ### Test token refresh logic [Section titled “Test token refresh logic”](#test-token-refresh-logic) ```python 1 def test_token_refresh_scenarios(self): 2 """Test various token refresh scenarios""" 3 test_cases = [ 4 { 5 "name": "successful_refresh", 6 "initial_status": "EXPIRED", 7 "expected_status": "ACTIVE", 8 "should_succeed": True 9 }, 10 { 11 "name": "refresh_token_invalid", 12 "initial_status": "EXPIRED", 13 "expected_status": "EXPIRED", 14 "should_succeed": False 15 }, 16 { 17 "name": "already_active", 18 "initial_status": "ACTIVE", 19 "expected_status": "ACTIVE", 20 "should_succeed": True 21 } 22 ] 23 24 for case in test_cases: 25 with self.subTest(case=case["name"]): 26 # Setup mock 27 mock_account = Mock() 28 mock_account.status = case["expected_status"] 29 30 if case["should_succeed"]: 31 self.actions.refresh_connected_account.return_value = mock_account 32 else: 33 self.actions.refresh_connected_account.side_effect = Exception("Refresh failed") 34 35 # Execute 36 try: 37 result = self.actions.refresh_connected_account( 38 identifier="test_user", 39 connection_name="gmail" 40 ) 41 success = True 42 except Exception: 43 success = False 44 45 # Assert 46 self.assertEqual(success, case["should_succeed"]) ``` ## Integration testing [Section titled “Integration testing”](#integration-testing) ### Test complete authentication flow [Section titled “Test complete authentication flow”](#test-complete-authentication-flow) ```python 1 import time 2 3 def test_complete_oauth_flow_integration(): 4 """ 5 Integration test for complete OAuth authentication flow. 6 Requires manual intervention for OAuth consent. 7 """ 8 user_id = "integration_test_user" 9 provider = "gmail" 10 11 # Step 1: Create connected account 12 print("Step 1: Creating connected account...") 13 response = actions.get_or_create_connected_account( 14 connection_name=provider, 15 identifier=user_id 16 ) 17 18 account = response.connected_account 19 assert account.status == "PENDING", f"Expected PENDING, got {account.status}" 20 print(f"✓ Connected account created: {account.id}") 21 22 # Step 2: Generate authorization link 23 print("\nStep 2: Generating authorization link...") 24 link_response = actions.get_authorization_link( 25 connection_name=provider, 26 identifier=user_id 27 ) 28 29 print(f"✓ Authorization link: {link_response.link}") 30 print("\n⚠ MANUAL STEP: Open this link in a browser and complete OAuth") 31 print(" Press Enter after completing OAuth flow...") 32 input() 33 34 # Step 3: Verify account is now active 35 print("\nStep 3: Verifying account status...") 36 time.sleep(2) # Brief delay for processing 37 38 account = actions.get_connected_account( 39 identifier=user_id, 40 connection_name=provider 41 ) 42 43 assert account.status == "ACTIVE", f"Expected ACTIVE, got {account.status}" 44 print(f"✓ Account is ACTIVE") 45 print(f" Granted scopes: {account.scopes}") 46 47 # Step 4: Test tool execution 48 print("\nStep 4: Testing tool execution...") 49 result = actions.execute_tool( 50 identifier=user_id, 51 tool_name="gmail_get_profile", 52 tool_input={} 53 ) 54 55 assert result is not None, "Tool execution failed" 56 print(f"✓ Tool executed successfully") 57 58 print("\n✓✓✓ Integration test completed successfully") 59 60 # Run with: pytest test_auth_integration.py -s (to see output) ``` ### Test error scenarios [Section titled “Test error scenarios”](#test-error-scenarios) ```python 1 def test_error_scenarios(): 2 """Test various error scenarios""" 3 user_id = "error_test_user" 4 5 # Test 1: Invalid provider 6 print("Test 1: Invalid provider...") 7 try: 8 actions.get_or_create_connected_account( 9 connection_name="invalid_provider", 10 identifier=user_id 11 ) 12 assert False, "Should have raised error" 13 except Exception as e: 14 print(f"✓ Caught expected error: {type(e).__name__}") 15 16 # Test 2: Execute tool without authentication 17 print("\nTest 2: Tool execution without auth...") 18 try: 19 actions.execute_tool( 20 identifier="nonexistent_user", 21 tool_name="gmail_send_email", 22 tool_input={"to": "test@example.com"} 23 ) 24 assert False, "Should have raised error" 25 except Exception as e: 26 print(f"✓ Caught expected error: {type(e).__name__}") 27 28 # Test 3: Missing required scopes 29 print("\nTest 3: Missing required scopes...") 30 # This test requires setup with insufficient scopes 31 print("⚠ Skipped: Requires special setup") 32 33 print("\n✓✓✓ Error scenario tests completed") ``` ## Automated testing [Section titled “Automated testing”](#automated-testing) ### Test authentication in CI/CD [Section titled “Test authentication in CI/CD”](#test-authentication-in-cicd) .github/workflows/test-auth.yml ```yaml 1 name: Test Authentication Flows 2 3 on: [push, pull_request] 4 5 jobs: 6 test: 7 runs-on: ubuntu-latest 8 9 steps: 10 - uses: actions/checkout@v2 11 12 - name: Set up Python 13 uses: actions/setup-python@v2 14 with: 15 python-version: '3.9' 16 17 - name: Install dependencies 18 run: | 19 pip install -r requirements.txt 20 pip install pytest pytest-cov 21 22 - name: Run unit tests 23 env: 24 SCALEKIT_CLIENT_ID: ${{ secrets.TEST_CLIENT_ID }} 25 SCALEKIT_CLIENT_SECRET: ${{ secrets.TEST_CLIENT_SECRET }} 26 SCALEKIT_ENV_URL: ${{ secrets.TEST_ENV_URL }} 27 run: | 28 pytest tests/test_auth.py -v --cov=src/auth 29 30 - name: Run integration tests (non-OAuth) 31 env: 32 SCALEKIT_CLIENT_ID: ${{ secrets.TEST_CLIENT_ID }} 33 SCALEKIT_CLIENT_SECRET: ${{ secrets.TEST_CLIENT_SECRET }} 34 SCALEKIT_ENV_URL: ${{ secrets.TEST_ENV_URL }} 35 run: | 36 pytest tests/test_auth_integration.py -v -k "not oauth" ``` ### Mock OAuth flows [Section titled “Mock OAuth flows”](#mock-oauth-flows) ```python 1 from unittest.mock import patch, Mock 2 3 def test_oauth_flow_with_mocks(): 4 """Test OAuth flow with mocked responses (no actual OAuth)""" 5 6 with patch('scalekit.actions.get_or_create_connected_account') as mock_create, \ 7 patch('scalekit.actions.get_authorization_link') as mock_link, \ 8 patch('scalekit.actions.get_connected_account') as mock_get: 9 10 # Mock connected account creation 11 mock_account = Mock() 12 mock_account.id = "account_123" 13 mock_account.status = "PENDING" 14 15 mock_response = Mock() 16 mock_response.connected_account = mock_account 17 mock_create.return_value = mock_response 18 19 # Mock authorization link 20 mock_link_response = Mock() 21 mock_link_response.link = "https://mock-oauth-url.com" 22 mock_link.return_value = mock_link_response 23 24 # Mock successful authentication (simulate user completing OAuth) 25 mock_account.status = "ACTIVE" 26 mock_account.scopes = ["gmail.readonly", "gmail.send"] 27 mock_get.return_value = mock_account 28 29 # Test the flow 30 # 1. Create account 31 response = mock_create(connection_name="gmail", identifier="user_123") 32 assert response.connected_account.status == "PENDING" 33 34 # 2. Get auth link 35 link = mock_link(connection_name="gmail", identifier="user_123") 36 assert "https://" in link.link 37 38 # 3. Simulate user completing OAuth (status changes to ACTIVE) 39 account = mock_get(identifier="user_123", connection_name="gmail") 40 assert account.status == "ACTIVE" 41 assert len(account.scopes) > 0 42 43 print("✓ OAuth flow test with mocks completed") ``` ## Performance testing [Section titled “Performance testing”](#performance-testing) ### Test token refresh performance [Section titled “Test token refresh performance”](#test-token-refresh-performance) ```python 1 import time 2 3 def test_token_refresh_performance(): 4 """Measure token refresh latency""" 5 user_id = "perf_test_user" 6 provider = "gmail" 7 8 # Setup: Create account with expired token 9 # (This requires manually setting up an expired account) 10 11 iterations = 10 12 refresh_times = [] 13 14 for i in range(iterations): 15 start_time = time.time() 16 17 try: 18 actions.refresh_connected_account( 19 identifier=user_id, 20 connection_name=provider 21 ) 22 elapsed = time.time() - start_time 23 refresh_times.append(elapsed) 24 print(f"Iteration {i+1}: {elapsed:.3f}s") 25 except Exception as e: 26 print(f"Iteration {i+1} failed: {e}") 27 28 if refresh_times: 29 avg_time = sum(refresh_times) / len(refresh_times) 30 min_time = min(refresh_times) 31 max_time = max(refresh_times) 32 33 print(f"\nToken Refresh Performance:") 34 print(f" Average: {avg_time:.3f}s") 35 print(f" Min: {min_time:.3f}s") 36 print(f" Max: {max_time:.3f}s") 37 38 # Assert reasonable performance (adjust threshold as needed) 39 assert avg_time < 2.0, f"Average refresh time too slow: {avg_time:.3f}s" ``` ## Best practices [Section titled “Best practices”](#best-practices) ### Test checklist [Section titled “Test checklist”](#test-checklist) 1. **Unit tests** - Test individual authentication functions 2. **Integration tests** - Test complete OAuth flows 3. **Error handling** - Test all error scenarios 4. **Token refresh** - Test automatic and manual refresh 5. **Multi-provider** - Test multiple simultaneous connections 6. **Performance** - Measure and optimize latency 7. **Security** - Verify token encryption and secure storage ### Testing dos and don’ts [Section titled “Testing dos and don’ts”](#testing-dos-and-donts) ✅ **Do:** * Use separate test accounts for each provider * Test both success and failure scenarios * Mock external OAuth calls in unit tests * Test token refresh before expiration * Verify error messages are helpful * Test with realistic data volumes ❌ **Don’t:** * Use production accounts for testing * Hardcode test credentials in source code * Skip error scenario testing * Assume OAuth always succeeds * Neglect performance testing * Test only happy path scenarios ### Security testing [Section titled “Security testing”](#security-testing) ```python 1 def test_security_scenarios(): 2 """Test security-related authentication scenarios""" 3 4 # Test 1: Verify tokens are not exposed in logs 5 print("Test 1: Token exposure check...") 6 with patch('logging.Logger.debug') as mock_log: 7 account = actions.get_connected_account( 8 identifier="test_user", 9 connection_name="gmail" 10 ) 11 12 # Verify no access tokens in log calls 13 for call in mock_log.call_args_list: 14 log_message = str(call) 15 assert "access_token" not in log_message.lower() 16 assert "refresh_token" not in log_message.lower() 17 18 print("✓ No tokens in logs") 19 20 # Test 2: Verify HTTPS for OAuth redirects 21 print("\nTest 2: HTTPS verification...") 22 link_response = actions.get_authorization_link( 23 connection_name="gmail", 24 identifier="test_user" 25 ) 26 27 assert link_response.link.startswith("https://") 28 print("✓ OAuth uses HTTPS") 29 30 # Test 3: State parameter validation 31 print("\nTest 3: State parameter present...") 32 assert "state=" in link_response.link 33 print("✓ State parameter included") 34 35 print("\n✓✓✓ Security tests completed") ``` ## Next steps [Section titled “Next steps”](#next-steps) * [Authentication Troubleshooting](/agent-auth/authentication/troubleshooting) - Debug authentication issues * [Multi-Provider Authentication](/agent-auth/authentication/multi-provider) - Test multiple providers --- # DOCUMENT BOUNDARY --- # Authentication Troubleshooting > Debug and resolve common authentication issues with Agent Auth, including OAuth failures, token problems, and provider-specific errors. This guide helps you diagnose and resolve common authentication issues with Agent Auth. Use the troubleshooting steps below to quickly identify and fix problems with connected accounts, OAuth flows, and token management. ## Quick diagnostics [Section titled “Quick diagnostics”](#quick-diagnostics) Start with these quick checks to identify the issue: ### Check connected account status [Section titled “Check connected account status”](#check-connected-account-status) * Python ```python 1 # Get connected account status 2 account = actions.get_connected_account( 3 identifier="user_123", 4 connection_name="gmail" 5 ) 6 7 print(f"Status: {account.status}") 8 print(f"Provider: {account.connection_name}") 9 print(f"Created: {account.created_at}") 10 print(f"Updated: {account.updated_at}") 11 12 # Status values: 13 # - PENDING: User hasn't completed authentication 14 # - ACTIVE: Connection is active and working 15 # - EXPIRED: Tokens expired, refresh may be needed 16 # - REVOKED: User revoked access 17 # - ERROR: Authentication error occurred ``` * Node.js ```javascript 1 // Get connected account status 2 const account = await scalekit.actions.getConnectedAccount({ 3 identifier: 'user_123', 4 connectionName: 'gmail' 5 }); 6 7 console.log(`Status: ${account.status}`); 8 console.log(`Provider: ${account.connectionName}`); 9 console.log(`Created: ${account.createdAt}`); 10 console.log(`Updated: ${account.updatedAt}`); 11 12 // Status values: 13 // - PENDING: User hasn't completed authentication 14 // - ACTIVE: Connection is active and working 15 // - EXPIRED: Tokens expired, refresh may be needed 16 // - REVOKED: User revoked access 17 // - ERROR: Authentication error occurred ``` * Go ```go 1 // Get connected account status 2 account, err := scalekitClient.Actions.GetConnectedAccount( 3 context.Background(), 4 "user_123", 5 "gmail", 6 ) 7 if err != nil { 8 log.Printf("Error getting account: %v", err) 9 return 10 } 11 12 fmt.Printf("Status: %s\n", account.Status) 13 fmt.Printf("Provider: %s\n", account.ConnectionName) 14 fmt.Printf("Created: %s\n", account.CreatedAt) 15 fmt.Printf("Updated: %s\n", account.UpdatedAt) ``` * Java ```java 1 // Get connected account status 2 ConnectedAccount account = scalekitClient.actions().getConnectedAccount( 3 "user_123", 4 "gmail" 5 ); 6 7 System.out.println("Status: " + account.getStatus()); 8 System.out.println("Provider: " + account.getConnectionName()); 9 System.out.println("Created: " + account.getCreatedAt()); 10 System.out.println("Updated: " + account.getUpdatedAt()); ``` ### Test tool execution [Section titled “Test tool execution”](#test-tool-execution) Try executing a simple tool to verify the connection: ```python 1 # Test with a simple read operation 2 try: 3 result = actions.execute_tool( 4 identifier="user_123", 5 tool_name='gmail_get_profile', # Simple read-only operation 6 tool_input={} 7 ) 8 print("✓ Connection working:", result) 9 except Exception as e: 10 print("✗ Connection failed:", str(e)) 11 # Error message provides clues about the issue ``` ## Common authentication errors [Section titled “Common authentication errors”](#common-authentication-errors) ### PENDING status - User hasn’t authenticated [Section titled “PENDING status - User hasn’t authenticated”](#pending-status---user-hasnt-authenticated) **Symptom:** Connected account status shows `PENDING` **Cause:** User created the connected account but hasn’t completed OAuth flow **Solution:** 1. Generate a new authorization link 2. Send it to the user via email, notification, or in-app message 3. User clicks link and completes authentication 4. Status changes to `ACTIVE` * Python ```python 1 # Generate authorization link for pending account 2 if account.status == "PENDING": 3 link_response = actions.get_authorization_link( 4 connection_name="gmail", 5 identifier="user_123" 6 ) 7 8 print(f"Send this link to user: {link_response.link}") 9 10 # In production: 11 # - Send email with the link 12 # - Show in-app notification 13 # - Display in user's settings page ``` * Node.js ```javascript 1 // Generate authorization link for pending account 2 if (account.status === 'PENDING') { 3 const linkResponse = await scalekit.actions.getAuthorizationLink({ 4 connectionName: 'gmail', 5 identifier: 'user_123' 6 }); 7 8 console.log(`Send this link to user: ${linkResponse.link}`); 9 10 // In production: 11 // - Send email with the link 12 // - Show in-app notification 13 // - Display in user's settings page 14 } ``` * Go ```go 1 // Generate authorization link for pending account 2 if account.Status == "PENDING" { 3 linkResponse, err := scalekitClient.Actions.GetAuthorizationLink( 4 context.Background(), 5 "gmail", 6 "user_123", 7 ) 8 if err != nil { 9 log.Fatal(err) 10 } 11 12 fmt.Printf("Send this link to user: %s\n", linkResponse.Link) 13 } ``` * Java ```java 1 // Generate authorization link for pending account 2 if ("PENDING".equals(account.getStatus())) { 3 AuthorizationLink linkResponse = scalekitClient.actions().getAuthorizationLink( 4 "gmail", 5 "user_123" 6 ); 7 8 System.out.println("Send this link to user: " + linkResponse.getLink()); 9 } ``` ### EXPIRED status - Tokens need refresh [Section titled “EXPIRED status - Tokens need refresh”](#expired-status---tokens-need-refresh) **Symptom:** Connected account status shows `EXPIRED` **Causes:** * Access token expired and automatic refresh failed * Refresh token became invalid * Provider temporarily unavailable during refresh **Solutions:** **Option 1: Try manual refresh** ```python 1 # Attempt manual token refresh 2 try: 3 account = actions.refresh_connected_account( 4 identifier="user_123", 5 connection_name="gmail" 6 ) 7 if account.status == "ACTIVE": 8 print("✓ Refresh successful") 9 else: 10 print("⚠ Refresh failed, user re-authentication needed") 11 except Exception as e: 12 print(f"✗ Refresh error: {e}") 13 # Proceed to Option 2 ``` **Option 2: Request user re-authentication** ```python 1 # If refresh fails, generate new authorization link 2 link_response = actions.get_authorization_link( 3 connection_name="gmail", 4 identifier="user_123" 5 ) 6 7 # Notify user to re-authenticate 8 print(f"Please re-authorize: {link_response.link}") ``` ### REVOKED status - User revoked access [Section titled “REVOKED status - User revoked access”](#revoked-status---user-revoked-access) **Symptom:** Connected account status shows `REVOKED` **Cause:** User revoked your application’s access through the provider’s settings (e.g., Google Account Settings, Microsoft Account Permissions) **Solution:** User must re-authenticate to restore access ```python 1 # For revoked accounts, only re-authentication works 2 if account.status == "REVOKED": 3 link_response = actions.get_authorization_link( 4 connection_name="gmail", 5 identifier="user_123" 6 ) 7 8 # Explain to user why re-authentication is needed 9 message = """ 10 Your Gmail connection was disconnected. 11 This may have happened if you: 12 - Revoked access in your Google Account settings 13 - Changed your Google password 14 - Enabled 2FA on your Google account 15 16 Please reconnect: {link} 17 """.format(link=link_response.link) 18 19 print(message) ``` Caution When a user revokes access, any pending tool executions will fail. Ensure your application handles `REVOKED` status gracefully and notifies users promptly. ## OAuth flow issues [Section titled “OAuth flow issues”](#oauth-flow-issues) ### Callback errors [Section titled “Callback errors”](#callback-errors) **Symptom:** OAuth redirect fails or returns error **Common errors and solutions:** | Error Code | Meaning | Solution | | --------------------- | --------------------------- | ------------------------------------------------ | | `access_denied` | User cancelled OAuth flow | Normal behavior, offer retry option | | `invalid_request` | Malformed OAuth request | Check OAuth parameters and scopes | | `unauthorized_client` | OAuth client not authorized | Verify OAuth credentials in Scalekit dashboard | | `invalid_scope` | Requested scope not valid | Review and correct requested scopes | | `server_error` | Provider error | Retry after a few minutes, check provider status | **Debugging callback issues:** ```python 1 # In your OAuth callback handler 2 def handle_oauth_callback(request): 3 error = request.args.get('error') 4 error_description = request.args.get('error_description') 5 code = request.args.get('code') 6 state = request.args.get('state') 7 8 if error: 9 # Log the error for debugging 10 print(f"OAuth error: {error}") 11 print(f"Description: {error_description}") 12 13 # Handle specific errors 14 if error == 'access_denied': 15 return "You cancelled the authorization. Please try again." 16 elif error == 'invalid_scope': 17 return "Invalid permissions requested. Please contact support." 18 else: 19 return f"Authorization failed: {error_description}" 20 21 if not code: 22 return "Missing authorization code" 23 24 # Continue with normal flow 25 # Scalekit handles the code exchange automatically 26 return "Authorization successful!" ``` ### Redirect URI mismatch [Section titled “Redirect URI mismatch”](#redirect-uri-mismatch) **Symptom:** Error message about redirect URI mismatch **Cause:** OAuth provider redirect URI doesn’t match configured URI in connection **Solution:** 1. Check the redirect URI in Scalekit dashboard 2. Navigate to **Connections** > Select connection > View **Redirect URI** 3. Copy the exact Scalekit redirect URI 4. Add it to your OAuth application in provider’s console (Google, Microsoft, etc.) 5. Ensure there are no trailing slashes or protocol mismatches (http vs https) Common redirect URI issues * **Trailing slashes**: `https://example.com/callback/` vs `https://example.com/callback` * **Protocol mismatch**: `http://` vs `https://` * **Port numbers**: Include port if required: `https://example.com:8080/callback` * **Subdomain changes**: Ensure subdomain matches exactly ### State parameter validation failure [Section titled “State parameter validation failure”](#state-parameter-validation-failure) **Symptom:** “Invalid state parameter” error **Cause:** State parameter doesn’t match or is missing (CSRF protection) **Solution:** This is handled automatically by Scalekit, but if you encounter this: 1. Ensure cookies are enabled in the browser 2. Check for clock skew between systems 3. Verify user isn’t switching browsers/devices mid-flow 4. Try clearing browser cookies and restarting flow ## Provider-specific issues [Section titled “Provider-specific issues”](#provider-specific-issues) ### Google Workspace [Section titled “Google Workspace”](#google-workspace) **Issue: “Access blocked: Authorization Error”** **Causes:** * App not verified by Google * Using restricted scopes * Domain admin restrictions **Solutions:** * Complete Google’s app verification process * Use less restrictive scopes during development * Contact domain admin to whitelist your app **Issue: “This app isn’t verified”** **Solution:** * Click “Advanced” → “Go to \[Your App] (unsafe)” for testing * Submit app for Google verification for production * Use Scalekit’s shared credentials for quick testing ### Microsoft 365 [Section titled “Microsoft 365”](#microsoft-365) **Issue: “AADSTS65001: User or administrator has not consented”** **Solution:** * Ensure required permissions are configured in Azure AD * Admin consent may be required for certain scopes * Check tenant-specific restrictions **Issue: “AADSTS50020: User account from identity provider does not exist”** **Solution:** * User must have a valid Microsoft 365 account * Check if user’s tenant allows external app access * Verify user’s email domain matches tenant ### Slack [Section titled “Slack”](#slack) **Issue: “OAuth access denied”** **Solution:** * User must have permission to install apps in their Slack workspace * Check workspace app approval settings * Ensure required scopes are not restricted by workspace admin **Issue: “Workspace installation restricted”** **Solution:** * Contact Slack workspace admin * Request app approval if workspace requires it * Use a different workspace for testing ## Tool execution failures [Section titled “Tool execution failures”](#tool-execution-failures) ### Authentication errors during execution [Section titled “Authentication errors during execution”](#authentication-errors-during-execution) **Symptom:** Tool execution fails with authentication error despite `ACTIVE` status **Debugging steps:** ```python 1 # Step 1: Verify account status 2 account = actions.get_connected_account( 3 identifier="user_123", 4 connection_name="gmail" 5 ) 6 print(f"Status: {account.status}") 7 8 # Step 2: Try to refresh tokens 9 try: 10 account = actions.refresh_connected_account( 11 identifier="user_123", 12 connection_name="gmail" 13 ) 14 print("✓ Token refresh successful") 15 except Exception as e: 16 print(f"✗ Token refresh failed: {e}") 17 18 # Step 3: Check granted scopes 19 print(f"Granted scopes: {account.scopes}") 20 # Verify the required scope for your tool is included 21 22 # Step 4: Try a simple read-only tool 23 try: 24 result = actions.execute_tool( 25 identifier="user_123", 26 tool_name='gmail_get_profile', 27 tool_input={} 28 ) 29 print("✓ Read operation successful") 30 except Exception as e: 31 print(f"✗ Read operation failed: {e}") ``` ### Insufficient permissions [Section titled “Insufficient permissions”](#insufficient-permissions) **Symptom:** “Insufficient permissions” or “Forbidden” error **Cause:** Required scope not granted during authentication **Solution:** 1. Check currently granted scopes 2. Determine required scopes for the tool 3. Request additional scopes by having user re-authenticate 4. Update connection scopes if needed ```python 1 # Check if specific scope is granted 2 required_scope = "https://www.googleapis.com/auth/gmail.send" 3 4 account = actions.get_connected_account( 5 identifier="user_123", 6 connection_name="gmail" 7 ) 8 9 if required_scope not in account.scopes: 10 print(f"⚠ Missing required scope: {required_scope}") 11 12 # Generate new authorization link with required scopes 13 link_response = actions.get_authorization_link( 14 connection_name="gmail", 15 identifier="user_123" 16 ) 17 18 print(f"User must re-authorize with additional permissions: {link_response.link}") ``` ## Connection configuration issues [Section titled “Connection configuration issues”](#connection-configuration-issues) ### Invalid OAuth credentials [Section titled “Invalid OAuth credentials”](#invalid-oauth-credentials) **Symptom:** “Invalid client” or “Client authentication failed” **Cause:** OAuth client ID or client secret incorrect or revoked **Solution:** 1. Navigate to Scalekit dashboard → **Connections** 2. Select the affected connection 3. Verify OAuth credentials match provider’s console 4. If using BYOC (Bring Your Own Credentials), double-check: * Client ID is correct * Client Secret hasn’t been regenerated * OAuth application is active in provider’s console 5. Update credentials if needed 6. Test connection with a new connected account ### Missing or incorrect scopes [Section titled “Missing or incorrect scopes”](#missing-or-incorrect-scopes) **Symptom:** Authorization succeeds but tool execution fails **Cause:** Connection configured with insufficient scopes **Solution:** ```python 1 # Check connection configuration in dashboard 2 # Ensure these scopes are configured: 3 4 # For Gmail: 5 # - https://www.googleapis.com/auth/gmail.readonly (read emails) 6 # - https://www.googleapis.com/auth/gmail.send (send emails) 7 # - https://www.googleapis.com/auth/gmail.modify (modify emails) 8 9 # For Google Calendar: 10 # - https://www.googleapis.com/auth/calendar.readonly (read calendar) 11 # - https://www.googleapis.com/auth/calendar.events (manage events) 12 13 # After updating scopes in connection, existing users must re-authenticate ``` ## Rate limiting and quota issues [Section titled “Rate limiting and quota issues”](#rate-limiting-and-quota-issues) ### Provider rate limits exceeded [Section titled “Provider rate limits exceeded”](#provider-rate-limits-exceeded) **Symptom:** “Rate limit exceeded” or “Quota exceeded” errors **Causes:** * Too many requests in short time period * Shared quota limits (when using Scalekit’s shared credentials) * Provider-specific rate limits **Solutions:** **Immediate:** * Implement exponential backoff and retry logic * Reduce request frequency * Batch operations where possible **Long-term:** * Use Bring Your Own Credentials for dedicated quotas * Implement request queuing * Cache frequently accessed data ```python 1 import time 2 from typing import Any, Dict 3 4 def execute_tool_with_retry( 5 identifier: str, 6 tool_name: str, 7 tool_input: Dict[str, Any], 8 max_retries: int = 3 9 ): 10 """Execute tool with exponential backoff retry logic""" 11 for attempt in range(max_retries): 12 try: 13 result = actions.execute_tool( 14 identifier=identifier, 15 tool_name=tool_name, 16 tool_input=tool_input 17 ) 18 return result 19 except Exception as e: 20 if "rate limit" in str(e).lower() and attempt < max_retries - 1: 21 # Exponential backoff: 1s, 2s, 4s 22 wait_time = 2 ** attempt 23 print(f"Rate limited, retrying in {wait_time}s...") 24 time.sleep(wait_time) 25 else: 26 raise 27 28 # Usage 29 result = execute_tool_with_retry( 30 identifier="user_123", 31 tool_name="gmail_send_email", 32 tool_input={"to": "user@example.com", "subject": "Test", "body": "Hello"} 33 ) ``` ## Network and connectivity issues [Section titled “Network and connectivity issues”](#network-and-connectivity-issues) ### Timeout errors [Section titled “Timeout errors”](#timeout-errors) **Symptom:** Requests timeout or take too long **Causes:** * Network connectivity issues * Provider API slow response * Large data transfers **Solutions:** * Increase timeout settings in your application * Implement async processing for slow operations * Check provider status page for known issues * Retry with exponential backoff ### SSL/TLS errors [Section titled “SSL/TLS errors”](#ssltls-errors) **Symptom:** SSL certificate verification failures **Causes:** * Outdated SSL certificates * Corporate proxy/firewall issues * System clock skew **Solutions:** * Update system CA certificates * Configure proxy settings if behind corporate firewall * Verify system clock is synchronized * Check firewall allows connections to Scalekit and provider domains ## Debugging tools and techniques [Section titled “Debugging tools and techniques”](#debugging-tools-and-techniques) ### Enable detailed logging [Section titled “Enable detailed logging”](#enable-detailed-logging) * Python ```python 1 import logging 2 3 # Enable debug logging for Scalekit SDK 4 logging.basicConfig(level=logging.DEBUG) 5 logger = logging.getLogger('scalekit') 6 logger.setLevel(logging.DEBUG) 7 8 # Now all API requests/responses will be logged 9 result = actions.execute_tool(...) ``` * Node.js ```javascript 1 // Enable debug mode in SDK initialization 2 const scalekit = new ScalekitClient({ 3 clientId: process.env.SCALEKIT_CLIENT_ID, 4 clientSecret: process.env.SCALEKIT_CLIENT_SECRET, 5 envUrl: process.env.SCALEKIT_ENV_URL, 6 debug: true // Enable detailed logging 7 }); ``` * Go ```go 1 // Enable debug logging 2 scalekitClient := scalekit.NewScalekitClient( 3 scalekit.WithClientID(os.Getenv("SCALEKIT_CLIENT_ID")), 4 scalekit.WithClientSecret(os.Getenv("SCALEKIT_CLIENT_SECRET")), 5 scalekit.WithEnvURL(os.Getenv("SCALEKIT_ENV_URL")), 6 scalekit.WithDebug(true), // Enable debug mode 7 ) ``` * Java ```java 1 // Enable debug logging 2 ScalekitClient scalekitClient = new ScalekitClient.Builder() 3 .clientId(System.getenv("SCALEKIT_CLIENT_ID")) 4 .clientSecret(System.getenv("SCALEKIT_CLIENT_SECRET")) 5 .envUrl(System.getenv("SCALEKIT_ENV_URL")) 6 .debug(true) // Enable debug mode 7 .build(); ``` ### Check Scalekit dashboard [Section titled “Check Scalekit dashboard”](#check-scalekit-dashboard) The Scalekit dashboard provides detailed information: 1. Navigate to **Agent Auth** → **Connected Accounts** 2. Find the affected connected account 3. View: * Current status and last updated time * Authentication events and errors * Token refresh history * Tool execution logs * Error messages and stack traces ### Test with curl [Section titled “Test with curl”](#test-with-curl) Test authentication directly with curl to isolate issues: ```bash 1 # Get connected account status 2 curl -X GET "https://api.scalekit.com/v1/connect/accounts/{account_id}" \ 3 -H "Authorization: Bearer YOUR_API_TOKEN" 4 5 # Refresh tokens 6 curl -X POST "https://api.scalekit.com/v1/connect/accounts/{account_id}/refresh" \ 7 -H "Authorization: Bearer YOUR_API_TOKEN" 8 9 # Execute tool 10 curl -X POST "https://api.scalekit.com/v1/connect/tools/execute" \ 11 -H "Authorization: Bearer YOUR_API_TOKEN" \ 12 -H "Content-Type: application/json" \ 13 -d '{ 14 "connected_account_id": "account_123", 15 "tool_name": "gmail_get_profile", 16 "tool_input": {} 17 }' ``` ## Getting help [Section titled “Getting help”](#getting-help) ### Information to provide [Section titled “Information to provide”](#information-to-provide) When contacting support, include: * **Connected Account ID**: Found in dashboard or API response * **Connection Name**: Which provider (gmail, slack, etc.) * **Error Messages**: Complete error text and stack traces * **Timestamp**: When the error occurred * **Steps to Reproduce**: What actions led to the error * **Expected Behavior**: What should have happened * **Environment**: Development, staging, or production ### Support channels [Section titled “Support channels”](#support-channels) * **Documentation**: Check related guides in docs * **Dashboard Logs**: Review logs in Scalekit dashboard * **Support Portal**: Submit ticket with details above * **Developer Community**: Ask questions in community forums * **Email Support**: for critical issues ## Next steps [Section titled “Next steps”](#next-steps) * [Scopes and Permissions](/agent-auth/authentication/scopes-permissions) - Managing OAuth scopes * [Multi-Provider Authentication](/agent-auth/authentication/multi-provider) - Managing multiple connections --- # DOCUMENT BOUNDARY --- # Auth patterns > Learn the supported auth types, provider payload fields, and auth pattern configuration used by bring your own provider. Auth patterns define how Scalekit should authenticate to the upstream API. Use this page to choose an auth type and build the provider payload for a custom provider. ## Choose an auth type [Section titled “Choose an auth type”](#choose-an-auth-type) Bring your own provider supports these auth types: * OAUTH * BASIC * BEARER * API\_KEY Use OAUTH when the upstream API requires a user authorization flow and token exchange. Use BASIC, BEARER, or API\_KEY when the upstream API accepts static credentials or long-lived tokens that Scalekit can attach during proxy calls. ## Understand the provider payload [Section titled “Understand the provider payload”](#understand-the-provider-payload) The provider payload uses these common top-level fields: * `display_name`: Human-readable name for the custom provider * `description`: Short description of what the provider connects to * `auth_patterns`: Authentication options supported by the provider * `proxy_url`: Base URL the proxy should call for the upstream API (mandatory) * `proxy_enabled`: Whether the proxy is enabled for the provider (mandatory, should be true) `proxy_url` can also include templated fields when the upstream API requires account-specific values, for example `https://{{domain}}/api`. ## Understand auth pattern fields [Section titled “Understand auth pattern fields”](#understand-auth-pattern-fields) 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 providers 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. These fields control how Scalekit collects credentials and applies them during proxy calls. ## Example JSON payloads [Section titled “Example JSON payloads”](#example-json-payloads) Use a payload like the following based on the auth pattern you need. In the management flow, you can pass these JSON bodies into the appropriate create or update request. * 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 } ``` ## Review the final payload carefully [Section titled “Review the final payload carefully”](#review-the-final-payload-carefully) When you review the final payload, pay close attention to: * `display_name` and `description` * The selected auth `type` * Required `fields` and `account_fields` * OAuth endpoints and scopes, if the provider uses OAuth * `proxy_url` Use the payload body from this page in the create and update requests on the [Managing providers](/agent-auth/bring-your-own-provider/managing-providers) page. --- # DOCUMENT BOUNDARY --- # Managing providers > Use the recommended skill to create, review, update, promote, and delete bring your own provider definitions. Use this page to create, list, update, promote, and delete custom providers in Scalekit. Create providers in `Dev` first, validate the definition, and then use the same definition for updates or Production rollout. Recommended approach The recommended way to manage providers is with the [`sk-actions-custom-provider` skill](https://github.com/scalekit-inc/skills/tree/main/skills/agent-auth/sk-actions-custom-provider). It keeps payload generation, review, and promotion consistent across Dev and Production. Use it to: * Infer the provider auth type * Generate the provider payload * Review existing providers before create or update * Show diffs before updates * Move provider definitions from Dev to Production * Prepare the delete curl for a provider Whenever the skill shows you the final provider payload, review the values carefully before approving or running the next step. If you prefer to manage custom providers with the APIs directly, use the `curl` commands on this page and the JSON bodies from [Auth patterns](/agent-auth/bring-your-own-provider/auth-types-and-patterns). Before using the `curl` examples on this page, make sure: * `SCALEKIT_ENVIRONMENT_URL` points to the Scalekit environment where you want to manage the provider * `env_access_token` contains a valid environment access token for that environment ## Create a provider [Section titled “Create a provider”](#create-a-provider) Create the provider in `Dev` first. Share the provider name, Scalekit credentials, API docs, auth docs, and base API URL if you already know it. The skill will infer the auth pattern, generate the provider payload, and help you create it. Use one of the JSON payloads from [Auth patterns](/agent-auth/bring-your-own-provider/auth-types-and-patterns) as the request body: ```bash 1 curl --location "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers" \ 2 --header "Authorization: Bearer $env_access_token" \ 3 --header "Content-Type: application/json" \ 4 --data '{...}' ``` After the provider is created, create a connection in the Scalekit Dashboard and continue with the standard Agent Auth flow. ## List providers [Section titled “List providers”](#list-providers) List existing providers before you create or update one. This helps you confirm whether the provider already exists and whether you should create a new provider or update an existing one. ```bash 1 curl --location "$SCALEKIT_ENVIRONMENT_URL/api/v1/providers?filter.provider_type=CUSTOM&page_size=1000" \ 2 --header "Authorization: Bearer $env_access_token" ``` ## Update a provider [Section titled “Update a provider”](#update-a-provider) When a provider changes, use the skill to compare the current provider with the proposed payload. Review auth fields, scopes, and proxy settings carefully before applying the update, because these values affect how future authorization and proxy calls behave. Use the [List providers](#list-providers) API to get the provider `identifier` from the response before sending the update request. Use the updated JSON body from [Auth patterns](/agent-auth/bring-your-own-provider/auth-types-and-patterns) and the provider `identifier` in the update request: ```bash 1 curl --location --request PUT "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers/$PROVIDER_IDENTIFIER" \ 2 --header "Authorization: Bearer $env_access_token" \ 3 --header "Content-Type: application/json" \ 4 --data '{...}' ``` ## Move a provider from Dev to Production [Section titled “Move a provider from Dev to Production”](#move-a-provider-from-dev-to-production) Use the `Dev` provider as the source of truth. The skill can locate the matching provider in `Dev`, compare it with `Production`, and prepare the correct action from there. In practice, this means: * fetch the provider definition from `Dev` * review the payload * create it in `Production` if it does not exist * update it in `Production` if it already exists Use the same JSON body from [Auth patterns](/agent-auth/bring-your-own-provider/auth-types-and-patterns) for the Production create or update request. This keeps the provider definition consistent between environments. ## Delete a provider [Section titled “Delete a provider”](#delete-a-provider) To delete a provider, resolve the correct provider identifier first. If the provider is still in use, remove the related connections or connected accounts before retrying the delete flow. Use the [List providers](#list-providers) API to get the provider `identifier` from the response before sending the delete request. ```bash 1 curl --location --request DELETE "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers/$PROVIDER_IDENTIFIER" \ 2 --header "Authorization: Bearer $env_access_token" ``` --- # DOCUMENT BOUNDARY --- # Bring your own provider > Add custom providers to Agent Auth and extend provider coverage while keeping authentication and authorization in Scalekit. Bring your own provider lets you add custom providers to Agent Auth when the API you need is not available as a built-in provider. Use bring your own provider to support unsupported SaaS APIs, partner systems, and internal APIs while keeping authentication, authorization, and secure API access in Scalekit. Once the provider is created, you use the same Agent Auth flow as other providers: create a connection, create or fetch a connected account, authorize the user, and call the upstream API through Tool Proxy. Custom providers appear alongside built-in providers when you create a connection in Scalekit: ![Custom provider shown alongside built-in providers in the provider selection view](/.netlify/images?url=_astro%2Fcustom-provider-in-catalog.BEwx1iKj.png\&w=2596\&h=1138\&dpl=69cce21a4f77360008b1503a) ## Why use bring your own provider [Section titled “Why use bring your own provider”](#why-use-bring-your-own-provider) Bring your own provider lets you: * Extend Agent Auth beyond the built-in provider catalog without inventing a separate auth stack * Bring unsupported SaaS APIs, partner systems, and internal APIs 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 provider definition to live upstream API calls through Tool Proxy using the same runtime model as other integrations Note Bring your own provider is for proxy-only connectors. ## How bring your own provider works [Section titled “How bring your own provider works”](#how-bring-your-own-provider-works) Bring your own provider uses the same Agent Auth model as built-in providers: 1. Create a provider definition 2. Create a connection in Scalekit Dashboard 3. Create a connected account and authorize the user 4. Use Tool Proxy to call the upstream API Creating the provider defines how Scalekit should authenticate to the upstream API. After that, connections, connected accounts, user authorization, and Tool Proxy work the same way as they do for built-in providers. --- # DOCUMENT BOUNDARY --- # Use Tool Proxy > Use Tool Proxy with your provider after creating a connection, authorizing a user, and setting up a connected account. Use this page to call a custom provider through Tool Proxy after the provider, connection, and connected account are set up. The provider definition controls how Scalekit authenticates the upstream API. Your application still uses a connection, a connected account, user authorization, and `actions.request(...)`. Bring your own provider does not introduce a separate runtime model. You still use the standard Agent Auth flow: * Create a connection for the provider in Scalekit Dashboard * Create or fetch the connected account for the user * Authorize the user if the connected account is not active * Call the upstream API through Tool Proxy Tool Proxy uses the connected account context to inject the correct authentication details before routing the request to the upstream API. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) Make sure: * The provider exists and is configured with the right [auth pattern](/agent-auth/bring-your-own-provider/auth-types-and-patterns) * A [connection](/agent-auth/connections) is configured for the provider * The [connected account](/agent-auth/connected-accounts) exists * The user has completed [authorization](/agent-auth/tools/authorize) Once these pieces are in place, you can call the upstream API through Tool Proxy. In the request examples below, `path` is relative to the provider `proxy_url`. `connectionName` must match the connection you created, and `identifier` must match the connected account you want to use for the request. After you create the provider, create a connection for it in the Scalekit Dashboard: ![Connections page showing a custom provider connection alongside built-in providers](/.netlify/images?url=_astro%2Fcustom-provider-connection.CmpN35cw.png\&w=2604\&h=762\&dpl=69cce21a4f77360008b1503a) After the user completes authorization, the connected account appears in the Connected Accounts tab and is ready for proxy calls: ![Connected Accounts tab showing an authenticated account for a custom provider](/.netlify/images?url=_astro%2Fcustom-provider-connected-account.CNBQ7XLh.png\&w=2610\&h=624\&dpl=69cce21a4f77360008b1503a) ## Proxy API calls [Section titled “Proxy API calls”](#proxy-api-calls) * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'your-provider-connection'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('Authorize provider:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1/customers', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "your-provider-connection" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("Authorize provider:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1/customers", 30 method="GET" 31 ) 32 print(result) ``` The request shape stays the same regardless of whether the provider uses OAUTH, BASIC, BEARER, or API\_KEY. The provider definition controls how Scalekit authenticates the upstream call. --- # DOCUMENT BOUNDARY --- # Code samples > Code samples of AI agents using Scalekit along with LangChain, Google ADK, and direct integrations ### [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) --- # DOCUMENT BOUNDARY --- # Connected accounts > Learn how to manage connected accounts in Agent Auth, including user authentication, authorization status, and account lifecycle management. Connected accounts in Agent Auth represent individual user or organization connections to third-party providers. They contain the authentication state, tokens, and permissions needed to execute tools on behalf of a specific identifier (user\_id, org\_id, or custom identifier). ## What are connected accounts? [Section titled “What are connected accounts?”](#what-are-connected-accounts) Connected accounts are the runtime instances that link your users to their third-party application accounts. Each connected account: * **Links to a connection**: Uses a pre-configured connection for authentication * **Has a unique identifier**: Associated with a user\_id, org\_id, or custom identifier * **Maintains auth state**: Tracks whether the user has completed authentication * **Stores tokens**: Securely holds access tokens and refresh tokens * **Manages permissions**: Tracks granted scopes and permissions ## Connected account lifecycle [Section titled “Connected account lifecycle”](#connected-account-lifecycle) Connected accounts go through several states during their lifecycle: ### Account states [Section titled “Account states”](#account-states) 1. **Pending**: Account created but user hasn’t completed authentication 2. **Active**: User has authenticated and tokens are valid 3. **Expired**: Tokens have expired and need refresh 4. **Revoked**: User has revoked access to the application 5. **Error**: Account has authentication or configuration errors 6. **Suspended**: Account temporarily disabled ### State transitions [Section titled “State transitions”](#state-transitions) ## Creating connected accounts [Section titled “Creating connected accounts”](#creating-connected-accounts) ### Using the dashboard [Section titled “Using the dashboard”](#using-the-dashboard) 1. **Navigate to connected accounts** in your Agent Auth dashboard 2. **Click create account** to start the process 3. **Select connection** to use for authentication 4. **Enter identifier** (user\_id, email, or custom identifier) 5. **Configure settings** such as scopes and permissions 6. **Generate auth URL** for the user to complete authentication 7. **Monitor status** until user completes the flow ### Using the API [Section titled “Using the API”](#using-the-api) Create connected accounts programmatically: * Python ```python 1 # actions = scalekit_client.actions (initialize ScalekitClient first — see quickstart) 2 response = actions.get_or_create_connected_account( 3 connection_name="gmail", 4 identifier="user_123" 5 ) 6 connected_account = response.connected_account 7 print(f"Connected account: {connected_account.id}, status: {connected_account.status}") ``` * Node.js ```typescript 1 // const actions = new ScalekitClient(ENV_URL, CLIENT_ID, CLIENT_SECRET).actions 2 const response = await actions.getOrCreateConnectedAccount({ 3 connectionName: 'gmail', 4 identifier: 'user_123', 5 }); 6 7 const connectedAccount = response.connectedAccount; 8 console.log('Connected account:', connectedAccount?.id, 'status:', connectedAccount?.status); ``` ## Authentication flow [Section titled “Authentication flow”](#authentication-flow) ### OAuth 2.0 flow [Section titled “OAuth 2.0 flow”](#oauth-20-flow) For OAuth connections, connected accounts follow the standard OAuth flow: 1. **Create connected account** with pending status 2. **Generate authorization URL** for the user 3. **User completes OAuth flow** with the third-party provider 4. **Provider redirects back** with authorization code 5. **Exchange code for tokens** and update account status 6. **Account becomes active** and ready for tool execution ### Authorization URL generation [Section titled “Authorization URL generation”](#authorization-url-generation) Generate URLs for users to complete authentication: * Python ```python 1 link_response = actions.get_authorization_link( 2 connection_name="gmail", 3 identifier="user_123" 4 ) 5 print(f"Authorization URL: {link_response.link}") 6 # Redirect the user to link_response.link to complete OAuth ``` * Node.js ```typescript 1 const linkResponse = await actions.getAuthorizationLink({ 2 connectionName: 'gmail', 3 identifier: 'user_123', 4 }); 5 6 console.log('Authorization URL:', linkResponse.link); 7 // Redirect the user to linkResponse.link to complete OAuth ``` ### Handling callbacks [Section titled “Handling callbacks”](#handling-callbacks) Scalekit handles the OAuth callback automatically. Once the user completes the authorization flow, Scalekit exchanges the code for tokens and updates the connected account status to `ACTIVE`. ## Managing connected accounts [Section titled “Managing connected accounts”](#managing-connected-accounts) ### Account information [Section titled “Account information”](#account-information) Retrieve connected account details and OAuth tokens: * Python ```python 1 response = actions.get_connected_account( 2 connection_name="gmail", 3 identifier="user_123" 4 ) 5 connected_account = response.connected_account 6 7 # Extract OAuth tokens from authorization details 8 tokens = connected_account.authorization_details["oauth_token"] 9 access_token = tokens["access_token"] 10 refresh_token = tokens["refresh_token"] 11 12 print(f"Account ID: {connected_account.id}") 13 print(f"Status: {connected_account.status}") ``` * Node.js ```typescript 1 const accountResponse = await actions.getConnectedAccount({ 2 connectionName: 'gmail', 3 identifier: 'user_123', 4 }); 5 6 const connectedAccount = accountResponse?.connectedAccount; 7 const authDetails = connectedAccount?.authorizationDetails; 8 9 // Extract OAuth tokens from authorization details 10 const accessToken = (authDetails && authDetails.details?.case === 'oauthToken') 11 ? authDetails.details.value?.accessToken 12 : undefined; 13 const refreshToken = (authDetails && authDetails.details?.case === 'oauthToken') 14 ? authDetails.details.value?.refreshToken 15 : undefined; 16 17 console.log('Account ID:', connectedAccount?.id); 18 console.log('Status:', connectedAccount?.status); ``` ### Token management [Section titled “Token management”](#token-management) Connected accounts automatically handle token lifecycle: **Automatic token refresh:** * Tokens are refreshed automatically before expiration * Refresh happens transparently during tool execution * Failed refresh attempts update account status to expired **Manual token refresh:** There is no SDK method to manually trigger a token refresh. If a connected account’s status is `EXPIRED` or `ERROR`, generate a new authorization link and prompt the user to re-authorize: * Python ```python 1 response = actions.get_or_create_connected_account( 2 connection_name="gmail", 3 identifier="user_123" 4 ) 5 connected_account = response.connected_account 6 7 if connected_account.status != "ACTIVE": 8 # Re-authorize the user to refresh their tokens 9 link_response = actions.get_authorization_link( 10 connection_name="gmail", 11 identifier="user_123" 12 ) 13 print(f"Re-authorization required. Send user to: {link_response.link}") ``` * Node.js ```typescript 1 const response = await actions.getOrCreateConnectedAccount({ 2 connectionName: 'gmail', 3 identifier: 'user_123', 4 }); 5 6 const connectedAccount = response.connectedAccount; 7 8 if (connectedAccount?.status !== 'ACTIVE') { 9 // Re-authorize the user to refresh their tokens 10 const linkResponse = await actions.getAuthorizationLink({ 11 connectionName: 'gmail', 12 identifier: 'user_123', 13 }); 14 console.log('Re-authorization required. Send user to:', linkResponse.link); 15 } ``` ### Account status monitoring [Section titled “Account status monitoring”](#account-status-monitoring) Monitor account authentication status: * Python ```python 1 response = actions.get_connected_account( 2 connection_name="gmail", 3 identifier="user_123" 4 ) 5 connected_account = response.connected_account 6 7 # Possible status values: 8 # - PENDING: Waiting for user authentication 9 # - ACTIVE: Authenticated and ready 10 # - EXPIRED: Tokens expired, needs re-authorization 11 # - REVOKED: User revoked access 12 # - ERROR: Authentication error 13 print(f"Account status: {connected_account.status}") ``` * Node.js ```typescript 1 const accountResponse = await actions.getConnectedAccount({ 2 connectionName: 'gmail', 3 identifier: 'user_123', 4 }); 5 6 const connectedAccount = accountResponse?.connectedAccount; 7 8 // Possible status values: 9 // - PENDING: Waiting for user authentication 10 // - ACTIVE: Authenticated and ready 11 // - EXPIRED: Tokens expired, needs re-authorization 12 // - REVOKED: User revoked access 13 // - ERROR: Authentication error 14 console.log('Account status:', connectedAccount?.status); ``` ## Account permissions and scopes [Section titled “Account permissions and scopes”](#account-permissions-and-scopes) Scopes define what actions a connected account can perform on a user’s behalf. Understanding how scopes are configured and updated is critical to building reliable agent integrations. ### Scopes are set at the connection level [Section titled “Scopes are set at the connection level”](#scopes-are-set-at-the-connection-level) Scopes are configured at the **connection level**, not at the individual connected account level. When a user completes the OAuth authorization flow for a connected account, they approve exactly the scopes defined on that connection. **Scopes are read-only after a connected account is created.** There is no API or SDK method to modify the granted scopes on an existing connected account after the user has completed authentication. ### Add or change scopes [Section titled “Add or change scopes”](#add-or-change-scopes) To request additional scopes for an existing connected account: 1. **Update the connection configuration** — In the Scalekit dashboard, navigate to the connection and add the new scopes. 2. **Generate a new magic link** — Use the Scalekit dashboard or API to create a new authorization link for the user. 3. **User approves the updated consent screen** — The user visits the link and approves the expanded OAuth consent screen with the new scopes. 4. **Connected account is updated** — After the user approves, Scalekit updates the connected account with the new token set. The user must go through the OAuth flow again whenever scopes change. There is no way to silently add scopes on their behalf. ### Account and connector status values [Section titled “Account and connector status values”](#account-and-connector-status-values) When working with connected accounts, you may encounter the following enum values from the Scalekit platform: **Connector status** | Value | Description | | --------------------------- | ----------------------------------------------------- | | `CONNECTOR_STATUS_ACTIVE` | Connector is configured and operational | | `CONNECTOR_STATUS_INACTIVE` | Connector is configured but not active | | `CONNECTOR_STATUS_PENDING` | Connector setup is incomplete | | `CONNECTOR_STATUS_ERROR` | Connector has a configuration or authentication error | **Connector type** | Value | Description | | --------------------------- | ------------------------------------------------- | | `CONNECTOR_TYPE_OAUTH2` | OAuth 2.0 connection (e.g., Gmail, Slack, GitHub) | | `CONNECTOR_TYPE_API_KEY` | API key-based connection (e.g., Zendesk, HubSpot) | | `CONNECTOR_TYPE_BASIC_AUTH` | Username and password connection | These values are returned in API responses when listing or inspecting connections and connected accounts. ## Account metadata and settings [Section titled “Account metadata and settings”](#account-metadata-and-settings) ### Custom metadata [Section titled “Custom metadata”](#custom-metadata) Custom metadata is managed via the Scalekit dashboard. ## Bulk operations [Section titled “Bulk operations”](#bulk-operations) ### Managing multiple accounts [Section titled “Managing multiple accounts”](#managing-multiple-accounts) The Python SDK supports read-only retrieval via `actions.get_connected_account` (Python) / `actions.getConnectedAccount` (Node.js). Use `actions.get_or_create_connected_account` when you need create-or-retrieve semantics. Bulk list and delete operations (`actions.listConnectedAccounts`, `actions.deleteConnectedAccount`) are available in the Node.js SDK, or via the direct API and dashboard. * Python ```python 1 # Get or create a connected account by identifier 2 response = actions.get_or_create_connected_account( 3 connection_name="gmail", 4 identifier="user_123" 5 ) 6 connected_account = response.connected_account 7 print(f"Account: {connected_account.id}, Status: {connected_account.status}") ``` * Node.js ```typescript 1 // List connected accounts 2 const listResponse = await actions.listConnectedAccounts({ 3 connectionName: 'gmail', 4 }); 5 console.log('Connected accounts:', listResponse); 6 7 // Delete a connected account 8 await actions.deleteConnectedAccount({ 9 connectionName: 'gmail', 10 identifier: 'user_123', 11 }); 12 console.log('Connected account deleted'); ``` ## Error handling [Section titled “Error handling”](#error-handling) ### Common errors [Section titled “Common errors”](#common-errors) Handle common connected account errors: * Python ```python 1 try: 2 response = actions.get_connected_account( 3 connection_name="gmail", 4 identifier="user_123" 5 ) 6 connected_account = response.connected_account 7 except Exception as e: 8 print(f"Error retrieving connected account: {e}") 9 # Check connected_account.status for EXPIRED, REVOKED, or ERROR states 10 # and prompt the user to re-authorize if needed ``` * Node.js ```typescript 1 try { 2 const accountResponse = await actions.getConnectedAccount({ 3 connectionName: 'gmail', 4 identifier: 'user_123', 5 }); 6 const connectedAccount = accountResponse?.connectedAccount; 7 console.log('Account status:', connectedAccount?.status); 8 } catch (error) { 9 console.error('Error retrieving connected account:', error); 10 // Check connectedAccount?.status for EXPIRED, REVOKED, or ERROR states 11 // and prompt the user to re-authorize if needed 12 } ``` ### Error recovery [Section titled “Error recovery”](#error-recovery) Implement error recovery strategies: 1. **Detect error** - Monitor account status and API responses 2. **Classify error** - Determine if error is recoverable 3. **Attempt recovery** - Try token refresh or re-authentication 4. **Notify user** - Inform user if manual action is required 5. **Update status** - Update account status based on recovery result ## Security considerations [Section titled “Security considerations”](#security-considerations) ### Token security [Section titled “Token security”](#token-security) Protect user tokens and credentials: * **Encryption**: All tokens are encrypted at rest and in transit * **Token rotation**: Implement regular token rotation * **Access logging**: Log all token access and usage * **Secure storage**: Use secure storage mechanisms for tokens ### Permission management [Section titled “Permission management”](#permission-management) Follow principle of least privilege: * **Minimal scopes**: Request only necessary permissions * **Scope validation**: Verify permissions before tool execution * **Regular audit**: Review granted permissions regularly * **User consent**: Ensure users understand granted permissions ### Account isolation [Section titled “Account isolation”](#account-isolation) Ensure proper account isolation: * **Tenant isolation**: Separate accounts by tenant/organization * **User isolation**: Prevent cross-user data access * **Connection isolation**: Separate different connection types * **Audit trail**: Maintain detailed audit logs ## Monitoring and analytics [Section titled “Monitoring and analytics”](#monitoring-and-analytics) ### Account health monitoring [Section titled “Account health monitoring”](#account-health-monitoring) Monitor the status of connected accounts by calling `actions.get_connected_account` (Python) or `actions.getConnectedAccount` (Node.js) and inspecting the `status` field. Accounts in a non-`ACTIVE` state may require re-authorization. ### Usage analytics [Section titled “Usage analytics”](#usage-analytics) Track usage patterns and tool execution results through the Scalekit dashboard. SDK methods for usage analytics are not currently available. ## Best practices [Section titled “Best practices”](#best-practices) ### Account lifecycle management [Section titled “Account lifecycle management”](#account-lifecycle-management) * **Regular cleanup**: Remove unused or expired accounts * **Status monitoring**: Monitor account status changes * **Proactive refresh**: Refresh tokens before expiration * **User notifications**: Notify users of authentication issues ### Performance optimization [Section titled “Performance optimization”](#performance-optimization) * **Connection pooling**: Reuse connections efficiently * **Token caching**: Cache tokens appropriately * **Batch operations**: Use bulk operations when possible * **Async processing**: Handle authentication flows asynchronously ### User experience [Section titled “User experience”](#user-experience) * **Clear error messages**: Provide helpful error messages to users * **Seamless re-auth**: Make re-authentication flows smooth * **Status visibility**: Show users their connection status * **Easy revocation**: Allow users to easily revoke access ## Testing connected accounts [Section titled “Testing connected accounts”](#testing-connected-accounts) ### Development testing [Section titled “Development testing”](#development-testing) Test connected accounts in development: * Python ```python 1 # Create or retrieve a test connected account 2 response = actions.get_or_create_connected_account( 3 connection_name="gmail", 4 identifier="test_user" 5 ) 6 test_account = response.connected_account 7 print(f"Test account: {test_account.id}, status: {test_account.status}") ``` * Node.js ```typescript 1 // Create or retrieve a test connected account 2 const response = await actions.getOrCreateConnectedAccount({ 3 connectionName: 'gmail', 4 identifier: 'test_user', 5 }); 6 7 const testAccount = response.connectedAccount; 8 console.log('Test account:', testAccount?.id, 'status:', testAccount?.status); ``` ### Integration testing [Section titled “Integration testing”](#integration-testing) Test authentication flows: 1. **Create test connection** with test credentials 2. **Create connected account** with test identifier 3. **Generate auth URL** and complete OAuth flow 4. **Verify account status** becomes active 5. **Test tool execution** with the account 6. **Test token refresh** and error scenarios Next, learn how to [authorize a user](/agent-auth/tools/authorize) so connected accounts can complete authentication before tool execution. --- # DOCUMENT BOUNDARY --- # Connections > Learn how to configure and manage connections in Agent Auth to enable authentication and tool execution with third-party providers. A **connection** is a configured integration between Scalekit and a third-party provider. It holds the credentials and settings Scalekit needs to interact with that provider’s API on behalf of your users — OAuth client secrets, API keys, scopes, and so on. You create one connection per provider in the Scalekit Dashboard. Once active, it can be shared across all your users. ## Connection types [Section titled “Connection types”](#connection-types) | Type | When to use | | ---------------- | --------------------------------------------------------------------------------- | | **OAuth 2.0** | Providers like Notion, Gmail, Slack, GitHub that use user-delegated authorization | | **API Key** | Providers like Exa, HarvestAPI, Snowflake that authenticate with a static key | | **Bearer token** | Providers that accept a long-lived bearer token | | **Basic auth** | Providers that use username + password authentication | ## Creating a connection [Section titled “Creating a connection”](#creating-a-connection) 1. Open the **Connections** section in the [Scalekit Dashboard](https://app.scalekit.com) 2. Click **Add connection** and select a provider 3. Enter the required credentials (OAuth client ID/secret, API key, etc.) 4. Save — the connection is now available for use For a step-by-step example, see how to set up a [Gmail connection](/reference/agent-connectors/gmail/). Next, learn how to create and manage [Connected accounts](/agent-auth/connected-accounts) that use these connections to authenticate and execute tools for your users. --- # DOCUMENT BOUNDARY --- # Agno > Learn how to use CrewAI with Agent Auth. # Agno [Section titled “Agno”](#agno) Agno is a framework for building AI agents. It provides a set of tools and abstractions for building AI agents. ## Usage [Section titled “Usage”](#usage) --- # DOCUMENT BOUNDARY --- # Anthropic > Learn how to enable tool calling with Agent Auth on top of Anthropic's models. # Anthropic [Section titled “Anthropic”](#anthropic) Anthropic is a framework for building AI agents. It provides a set of tools and abstractions for building AI agents. ## Usage [Section titled “Usage”](#usage) --- # DOCUMENT BOUNDARY --- # Google ADK > Build a Google ADK agent that authenticates users with Gmail and executes tools on their behalf using Scalekit Agent Auth. Build a Gmail-powered AI agent using Google ADK that authenticates users via OAuth 2.0 and fetches their last 5 unread emails using Scalekit Agent Auth. Visit the [Google ADK quickstart guide](https://google.github.io/adk-docs/get-started/quickstart/) to set up Google ADK before starting. [Full code on GitHub ](https://github.com/scalekit-inc/google-adk-agent-example) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) Before you start, ensure you have: * **Scalekit account and API credentials** — Get your Client ID and Client Secret from the [Scalekit dashboard](https://app.scalekit.com) * **Google API Key** — Obtain from [Google AI Studio](https://aistudio.google.com) * **Gmail account** — For testing the OAuth authentication flow 1. ### Set up your environment [Section titled “Set up your environment”](#set-up-your-environment) Install the Scalekit Python SDK and Google ADK, then initialize the client. Get your credentials from **Developers → Settings → API Credentials** in the [dashboard](https://app.scalekit.com). ```sh pip install scalekit-sdk-python google-adk ``` ```python import scalekit.client import os scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENV_URL"), ) actions = scalekit_client.actions ``` 2. ### Create a connected account [Section titled “Create a connected account”](#create-a-connected-account) Authorize a user to access their Gmail account by creating a connected account. This represents the user’s connection to their Gmail account: ```python 1 # Create a connected account for user if it doesn't exist already 2 response = actions.get_or_create_connected_account( 3 connection_name="GMAIL", 4 identifier="user_123" 5 ) 6 connected_account = response.connected_account 7 8 print(f'Connected account created: {connected_account.id}') ``` 3. ### Authenticate the user [Section titled “Authenticate the user”](#authenticate-the-user) For Scalekit to execute tools on behalf of the user, the user must grant authorization to access their Gmail account. Scalekit automatically handles the entire OAuth workflow, including token refresh. ```python 1 # Check if the user needs to authorize their Gmail connection 2 # This handles both new users and cases where the access token has expired 3 if connected_account.status != "ACTIVE": 4 # Generate an authorization link for the user to complete OAuth flow 5 link_response = actions.get_authorization_link( 6 connection_name="GMAIL", 7 identifier="user_123" 8 ) 9 10 # Display the authorization link to the user 11 print(f"🔗 Authorize Gmail access: {link_response.link}") 12 input("⎆ Press Enter after completing authorization...") 13 14 # In production, redirect users to the authorization URL instead of using input() 15 # The user will be redirected back to your app after successful authorization ``` 4. ### Build a Google ADK agent [Section titled “Build a Google ADK agent”](#build-a-google-adk-agent) Build a simple agent that fetches the last 5 unread emails from the user’s inbox and generates a summary. ```python 1 from google.adk.agents import Agent 2 3 # Get Gmail tools from Scalekit in Google ADK format 4 gmail_tools = actions.google.get_tools( 5 providers=["GMAIL"], 6 identifier="user_123", 7 page_size=100 8 ) 9 10 # Create a Google ADK agent with Gmail tools 11 gmail_agent = Agent( 12 name="gmail_assistant", 13 model="gemini-2.5-flash", 14 description="Gmail assistant that can read and manage emails", 15 instruction=( 16 "You are a helpful Gmail assistant. You can read, send, and organize emails. " 17 "When asked to fetch emails, focus on unread messages and provide clear summaries. " 18 "Always be helpful and professional." 19 ), 20 tools=gmail_tools 21 ) 22 23 # Use the agent to fetch and summarize unread emails 24 response = gmail_agent.process_request( 25 "fetch my last 5 unread emails and summarize them" 26 ) 27 print(response) ``` --- # DOCUMENT BOUNDARY --- # Google GenAI > Learn how to use Google GenAI with Agent Auth. # Google GenAI [Section titled “Google GenAI”](#google-genai) Google GenAI is a framework for building AI agents. It provides a set of tools and abstractions for building AI agents. ## Usage [Section titled “Usage”](#usage) --- # DOCUMENT BOUNDARY --- # LangChain > Build a LangChain agent that authenticates users with Gmail and executes tools on their behalf using Scalekit Agent Auth. This guide shows how to build a LangChain agent that authenticates users with Gmail via OAuth 2.0 and fetches their last 5 unread emails using Scalekit Agent Auth. [Full code on GitHub ](https://github.com/scalekit-inc/sample-langchain-agent) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) Before you start, make sure you have: * **Scalekit API credentials** — Client ID and Client Secret from [app.scalekit.com](https://app.scalekit.com) * **OpenAI API Key** — Get from [OpenAI Platform](https://platform.openai.com/api-keys) 1. ### Set up your environment [Section titled “Set up your environment”](#set-up-your-environment) Install the Scalekit Python SDK and initialize the client. Get your credentials from **Developers → Settings → API Credentials** in the [dashboard](https://app.scalekit.com). ```sh pip install scalekit-sdk-python langchain langchain-openai ``` ```python import scalekit.client import os 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_ENV_URL"), ) actions = scalekit_client.actions ``` 2. ### Create a connected account [Section titled “Create a connected account”](#create-a-connected-account) Authorize a user to access their Gmail account by creating a connected account. This represents the user’s connection to their Gmail account: ```python 1 # Create a connected account for user if it doesn't exist already 2 response = actions.get_or_create_connected_account( 3 connection_name="gmail", 4 identifier="user_123" # unique identifier; replace with your system's user ID 5 ) 6 connected_account = response.connected_account 7 8 print(f'Connected account created: {connected_account.id}') ``` 3. ### Authenticate the user [Section titled “Authenticate the user”](#authenticate-the-user) For Scalekit to execute tools on behalf of the user, the user must grant authorization to access their Gmail account. Scalekit automatically handles the entire OAuth workflow, including token refresh. ```python 1 # If the user hasn't yet authorized the gmail connection or if the user's access token is expired, 2 # generate a magic link and redirect the user to this link so that the user can complete authorization 3 if(connected_account.status != "ACTIVE"): 4 link_response = actions.get_authorization_link( 5 connection_name="gmail", 6 identifier="user_123" 7 ) 8 print(f"🔗click on the link to authorize gmail", link_response.link) 9 input(f"⎆ Press Enter after authorizing gmail...") 10 11 # In a real app, redirect the user to this URL so that the user can complete the authentication process for their gmail account ``` 4. ### Build a LangChain agent [Section titled “Build a LangChain agent”](#build-a-langchain-agent) Build a simple agent that fetches the last 5 unread emails from the user’s inbox and generates a summary. ```python 1 from langchain_openai import ChatOpenAI 2 from langchain.agents import AgentExecutor, create_openai_tools_agent 3 from langchain_core.prompts import SystemMessagePromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate,PromptTemplate, ChatPromptTemplate 4 5 # use scalekit SDK to fetch all GMAIL tools in langchain format 6 tools = actions.langchain.get_tools( 7 identifier="user_123", 8 providers = ["GMAIL"], # all tools for provider used by default 9 page_size=100 10 ) 11 12 prompt = ChatPromptTemplate.from_messages([ 13 SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], template="You are a helpful assistant. Use tools if needed")), 14 MessagesPlaceholder(variable_name="chat_history", optional=True), 15 HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=["input"], template="{input}")), 16 MessagesPlaceholder(variable_name="agent_scratchpad") 17 ]) 18 19 llm = ChatOpenAI(model="gpt-4o") 20 agent = create_openai_tools_agent(llm, tools, prompt) 21 agent_executor = AgentExecutor(agent=agent, tools=tools,verbose=True) 22 result = agent_executor.invoke({"input":"fetch my last 5 unread emails and summarize it"} 23 ) ``` --- # DOCUMENT BOUNDARY --- # Mastra > Learn how to use Mastra with Agent Auth. # Mastra [Section titled “Mastra”](#mastra) Mastra is a framework for building AI agents. It provides a set of tools and abstractions for building AI agents. ## Usage [Section titled “Usage”](#usage) --- # DOCUMENT BOUNDARY --- # MCP > Learn how to use MCP with Agent Auth. # MCP [Section titled “MCP”](#mcp) MCP is a framework for building AI agents. It provides a set of tools and abstractions for building AI agents. ## Usage [Section titled “Usage”](#usage) --- # DOCUMENT BOUNDARY --- # OpenAI > Learn how to use OpenAI with Agent Auth. # OpenAI [Section titled “OpenAI”](#openai) OpenAI is a framework for building AI agents. It provides a set of tools and abstractions for building AI agents. ## Usage [Section titled “Usage”](#usage) --- # DOCUMENT BOUNDARY --- # Vercel > Learn how to use Vercel with Agent Auth. # Vercel [Section titled “Vercel”](#vercel) Vercel is a framework for building AI agents. It provides a set of tools and abstractions for building AI agents. ## Usage [Section titled “Usage”](#usage) --- # DOCUMENT BOUNDARY --- # Give your agent tool access via MCP > Create a per-user MCP server with whitelisted, pre-authenticated tools — then hand your agent a single URL. When your agent needs to act on behalf of a user — reading their email, creating calendar events — each user must authenticate to each service separately. Managing those credentials in your agent adds complexity and security risk. Scalekit solves this with per-user MCP servers. You define which tools and connections a server exposes, and Scalekit gives you a unique, pre-authenticated URL for each user. Hand that URL to your agent — it calls tools through MCP, Scalekit handles the auth. MCP servers only support Streamable HTTP transport. Testing only — not for production This feature is in beta and intended for testing purposes only. Do not use it in production environments. ## How it works [Section titled “How it works”](#how-it-works) Two objects are central to this model: | Object | What it is | Created | | ---------------- | ------------------------------------------------------------------------ | ----------------- | | **MCP config** | A reusable template that defines which connections and tools are exposed | Once, by your app | | **MCP instance** | A per-user instantiation of a config, with its own URL | Once per user | Your app creates a config once, then calls `ensure_instance` whenever a new user needs access. Scalekit generates a unique URL for that user. When the agent calls tools through that URL, Scalekit routes each call using the user’s pre-authorized credentials. ![Architecture diagram showing how Scalekit MCP works: app creates a config, Scalekit creates per-user instances with unique URLs, users authorize OAuth connections, and the agent connects via MCP URL](/.netlify/images?url=_astro%2Fmcp-tool-access-architecture.Df4E84fg.png\&w=6920\&h=1320\&dpl=69cce21a4f77360008b1503a) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) Before you start, make sure you have: * **Scalekit API credentials** — Go to **Dashboard → Settings** and copy your `environment_url`, `client_id` and `client_secret` * **Gmail and Google Calendar connections configured in Scalekit:** * **Gmail**: **Dashboard → Connections** (Agent Auth) → Create Connection → Gmail → set `Connection Name = MY_GMAIL` → Save * **Google Calendar**: **Dashboard → Connections** (Agent Auth) → Create Connection → Google Calendar → set `Connection Name = MY_CALENDAR` → Save 1. ## Install the SDK and initialize the client [Section titled “Install the SDK and initialize the client”](#install-the-sdk-and-initialize-the-client) Install the Scalekit Python SDK: ```sh pip install scalekit-sdk-python python-dotenv>=1.0.1 ``` Initialize the client using your environment credentials: ```python import os import scalekit.client from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENV_URL"), ) my_mcp = scalekit_client.actions.mcp ``` 2. ## Create an MCP config [Section titled “Create an MCP config”](#create-an-mcp-config) An MCP config is a reusable template. It declares which connections and tools your server exposes. Create it once — not once per user. ```python cfg_response = my_mcp.create_config( name="reminder-manager", description="Summarizes latest email and creates a reminder event", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="MY_GMAIL", tools=["gmail_fetch_mails"], ), McpConfigConnectionToolMapping( connection_name="MY_CALENDAR", tools=["googlecalendar_create_event"], ), ], ) config_name = cfg_response.config.name ``` 3. ## Get a per-user MCP URL [Section titled “Get a per-user MCP URL”](#get-a-per-user-mcp-url) Call `ensure_instance` to get a unique MCP URL for a specific user. If an instance already exists for that user, Scalekit returns it — so it’s safe to call on every login. ```python inst_response = my_mcp.ensure_instance( config_name=config_name, user_identifier="john-doe", ) mcp_url = inst_response.instance.url print("MCP URL:", mcp_url) ``` Before the agent can use this URL, the user must authorize each connection. Retrieve the auth links and surface them to the user: ```python auth_state_response = my_mcp.get_instance_auth_state( instance_id=inst_response.instance.id, include_auth_links=True, ) for conn in getattr(auth_state_response, "connections", []): print("Connection:", conn.connection_name, "| Status:", conn.connected_account_status, "| Auth link:", conn.authentication_link) ``` Complete authentication Open the printed links in your browser and complete authentication for each connection. In production, surface these links to users via your app UI, email, or a Slack message. Poll `get_instance_auth_state` (without `include_auth_links`) to check when a user has completed authorization before passing the URL to your agent. At this point you have a per-user MCP URL. You can pass it to any spec-compliant MCP client — MCP Inspector, Claude Desktop, or an agent framework. The next step shows an example using LangChain. 4. ## Connect an agent (LangChain example) [Section titled “Connect an agent (LangChain example)”](#connect-an-agent-langchain-example) Install the LangChain dependencies: ```sh pip install langgraph>=0.6.5 langchain-mcp-adapters>=0.1.9 openai>=1.53.0 ``` Set your OpenAI API key: ```sh export OPENAI_API_KEY=your-openai-api-key ``` Pass the MCP URL to a LangChain agent. The agent discovers available tools automatically — no additional auth configuration required: ```python import asyncio from langgraph.prebuilt import create_react_agent from langchain_mcp_adapters.client import MultiServerMCPClient async def run_agent(mcp_url: str): client = MultiServerMCPClient( { "reminder_demo": { "transport": "streamable_http", "url": mcp_url, }, } ) tools = await client.get_tools() agent = create_react_agent("openai:gpt-4.1", tools) response = await agent.ainvoke({ "messages": "Get my latest email and create a calendar reminder in the next 15 minutes." }) print(response) asyncio.run(run_agent(mcp_url)) ``` MCP client compatibility This MCP server works with MCP Inspector, Claude Desktop, and any spec-compliant MCP client. ChatGPT’s beta connector may not work correctly — it is still in beta and does not fully implement the MCP specification. Full working code for all steps above is on [GitHub](https://github.com/scalekit-inc/python-connect-demos/tree/main/mcp). ## Next steps [Section titled “Next steps”](#next-steps) [LangChain integration ](/agent-auth/frameworks/langchain)Use LangChain to build agents that connect to Scalekit MCP servers. [Google ADK integration ](/agent-auth/frameworks/google-adk)Connect Scalekit tools to agents built with Google's Agent Development Kit. [Manage connections ](/agent-auth/connections)Learn how to configure and manage provider connections in Scalekit. --- # DOCUMENT BOUNDARY --- # OpenClaw skill > Connect OpenClaw agents to third-party services through Scalekit. Supports LinkedIn, Notion, Slack, Gmail, and 50+ providers. Use the Scalekit Agent Auth skill for [OpenClaw](https://github.com/scalekit-inc/openclaw-skill) to let your AI agents execute actions on third-party services directly from conversations. Search LinkedIn, read Notion pages, send Slack messages, query Snowflake, and more — all through Scalekit Connect without storing tokens or API keys in your agent. Security considerations for AI agents Scalekit stores tokens and API keys securely with full audit logging. OpenClaw, like all AI agent frameworks, is vulnerable to prompt injection and other agent-level attacks. Follow security best practices to protect your instance. When you ask Claude to interact with a third-party service, the skill: * Finds the configured provider in Scalekit (e.g., [Gmail connection setup](/reference/agent-connectors/gmail/)) and identifies which connection to use based on the requested action * Checks if the connection is active. For OAuth connections, it generates a magic link for new authorizations. For API key connections, it provides Dashboard guidance for setup * Retrieves available tools and their parameter schemas for the provider, determining what actions are possible * Calls the right tool with the correct parameters and returns the result to your conversation * If no tool exists for the action, routes the request through Scalekit’s HTTP proxy, making direct API calls on your behalf Automatic auth flow detection The skill automatically detects whether a connection uses OAuth or an API key and applies the correct auth flow — no configuration needed. Your agent never stores tokens or API keys. Scalekit acts as a token vault, managing all OAuth tokens, API keys, and credentials. The skill retrieves only what it needs at runtime, scoped to the requesting user. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * [OpenClaw](https://openclaw.ai) installed and configured * A Scalekit account with Agent Auth enabled — [sign up at app.scalekit.com](https://app.scalekit.com) * `python3` and `uv` available in your PATH ## Get started [Section titled “Get started”](#get-started) 1. ## Install the skill [Section titled “Install the skill”](#install-the-skill) Install the skill from ClawHub: ```bash clawhub install scalekit-agent-auth ``` 2. ## Configure credentials [Section titled “Configure credentials”](#configure-credentials) Add your Scalekit credentials to `.env` in your project root: .env ```bash 1 TOOL_CLIENT_ID=skc_your_client_id # Your Scalekit client ID 2 TOOL_CLIENT_SECRET=your_client_secret # Your Scalekit client secret 3 TOOL_ENV_URL=https://your-env.scalekit.cloud # Your Scalekit environment URL 4 TOOL_IDENTIFIER=your_default_user_identifier # Default user context for tool calls ``` | Parameter | Description | | -------------------- | --------------------------------------------------- | | `TOOL_CLIENT_ID` | Your Scalekit client ID Required | | `TOOL_CLIENT_SECRET` | Your Scalekit client secret Required | | `TOOL_ENV_URL` | Your Scalekit environment URL Required | | `TOOL_IDENTIFIER` | Default user context for all tool calls Recommended | Environment variable security Never commit `.env` files to version control. Add `.env` to your `.gitignore` file to prevent accidental exposure of credentials. 3. ## Usage [Section titled “Usage”](#usage) * Gmail ```txt You: Show me my latest unread emails ``` OpenClaw will automatically: 1. Look up the `GMAIL` connection 2. Verify it’s active (or generate a magic link to authorize if needed) 3. Fetch the `gmail_list_emails` tool schema 4. Return your latest unread emails * Notion ```txt You: Read my Notion page https://notion.so/My-Page-abc123 ``` OpenClaw will: 1. Look up the `NOTION` connection 2. If not yet authorized, generate a magic link for you to complete OAuth 3. Fetch the `notion_page_get` tool schema 4. Return the page content ## Supported providers [Section titled “Supported providers”](#supported-providers) Any provider configured in Scalekit works with the OpenClaw skill — including Notion, Slack, Gmail, Google Sheets, GitHub, Salesforce, HubSpot, Linear, Snowflake, Exa, HarvestAPI, and 50+ more. [Browse connections ](/guides/integrations/agent-connectors)See all supported providers in the Scalekit dashboard [ClawHub listing ](https://clawhub.dev/skills/scalekit-agent-auth)Install scalekit-agent-auth from ClawHub ## Common scenarios [Section titled “Common scenarios”](#common-scenarios) How do I authorize a new connection? When you request an action for a connection that isn’t yet authorized, the skill automatically generates a magic link. Click the link to complete OAuth authorization in your browser. After authorization, return to your OpenClaw conversation and retry the action. For API key-based connections (like Snowflake), you’ll need to configure credentials directly in the Scalekit Dashboard under **Connections**. How do I switch between different user contexts? Set `TOOL_IDENTIFIER` in your `.env` file to define a default user context. All tool calls will execute with that user’s permissions and connected accounts. To use a different user context for a specific conversation, you can override the identifier by setting it in your OpenClaw configuration or passing it as a parameter when invoking the skill. Why am I seeing a “connection not found” error? This error occurs when the skill cannot find a configured connection for the requested provider. Check the following: 1. **Verify the connection exists** — Go to **Dashboard > Connections** and confirm the provider is configured 2. **Check connection status** — Ensure the connection shows as “Active” in the dashboard 3. **Verify environment** — Confirm you’re using the correct `TOOL_ENV_URL` for your environment How do I debug tool execution issues? Enable debug logging in your OpenClaw configuration to see detailed information about tool calls: ```bash TOOL_DEBUG=true ``` This logs the tool name, parameters, and response for each execution, helping you identify issues with parameter formatting or API responses. --- # DOCUMENT BOUNDARY --- # Overview > Learn about Agent Auth, Scalekit's authentication solution for securely connecting to third-party applications through OAuth 2.0 Agent Auth is Scalekit’s authentication solution that enables your applications to securely connect to third-party services on behalf of your users. It handles the complexity of OAuth flows, token management, and multi-provider authentication for popular business applications like Gmail, Slack, Jira, and more. ## What is Agent Auth? [Section titled “What is Agent Auth?”](#what-is-agent-auth) Agent Auth simplifies third-party authentication by providing: * **Multi-provider OAuth**: Support for OAuth 2.0 flows across all major providers * **Unified authentication API**: Single interface for managing connections to any provider * **Automatic token management**: Token refresh, storage, and lifecycle handling * **Flexible authentication modes**: Use Scalekit-managed OAuth or bring your own credentials * **Secure token handling**: Encrypted token storage and transmission ## Key concepts [Section titled “Key concepts”](#key-concepts) ### Providers [Section titled “Providers”](#providers) Providers are third-party applications that your users can authenticate with. Agent Auth supports OAuth 2.0 flows for popular platforms including Gmail, Slack, GitHub, Jira, and many more. ### Connections [Section titled “Connections”](#connections) Connections define the authentication configuration for a specific provider. Each connection contains: * **Authentication credentials** (OAuth client ID/secret, API keys) * **OAuth configuration** (authorization URLs, token endpoints, scopes) * **Provider-specific settings** (rate limits, API versions) ### Connected accounts [Section titled “Connected accounts”](#connected-accounts) Connected accounts represent the authenticated link between a user in your application and their account on a third-party provider. Each connected account maintains: * **Authentication state** (active, expired, revoked) * **Access tokens** and refresh tokens with automatic refresh handling * **Granted OAuth scopes** and permissions * **Token expiration** tracking and lifecycle management ## Architecture overview [Section titled “Architecture overview”](#architecture-overview) ## How authentication works [Section titled “How authentication works”](#how-authentication-works) ### 1. Configure provider connections [Section titled “1. Configure provider connections”](#1-configure-provider-connections) Set up OAuth credentials for each provider you want to support: ```javascript 1 // Create a connection for Gmail with OAuth 2.0 2 const gmailConnection = await agentConnect.connections.create({ 3 provider: 'gmail', 4 auth_type: 'oauth2', 5 credentials: { 6 client_id: 'your-gmail-client-id', 7 client_secret: 'your-gmail-client-secret' 8 }, 9 scopes: ['https://www.googleapis.com/auth/gmail.send'] 10 }); ``` ### 2. Initiate OAuth flow for users [Section titled “2. Initiate OAuth flow for users”](#2-initiate-oauth-flow-for-users) When users want to connect their accounts, create a connected account and redirect them to complete OAuth: ```javascript 1 // Create a connected account for a user 2 const connectedAccount = await agentConnect.accounts.create({ 3 connection_id: gmailConnection.id, 4 identifier: 'user_123', 5 identifier_type: 'user_id' 6 }); 7 8 // Generate OAuth authorization URL 9 const authUrl = await agentConnect.accounts.getAuthUrl(connectedAccount.id); 10 // Redirect user to authUrl to authenticate with the provider ``` ### 3. Handle OAuth callback [Section titled “3. Handle OAuth callback”](#3-handle-oauth-callback) After the user authorizes your application, the provider redirects back with an authorization code. Agent Auth automatically exchanges this code for access and refresh tokens: ```javascript 1 // Agent Auth handles the OAuth callback and token exchange 2 // Tokens are securely stored and automatically refreshed when needed 3 const account = await agentConnect.accounts.get(connectedAccount.id); 4 // account.status will be 'active' once authentication is complete ``` ## Supported providers [Section titled “Supported providers”](#supported-providers) Agent Auth supports OAuth authentication for a wide range of popular business applications: Communication * Gmail (Google Workspace) * Outlook (Microsoft 365) * Slack * Microsoft Teams * Discord Productivity * Google Calendar * Microsoft Calendar * Google Drive * OneDrive * Notion Project Management * Jira * Asana * Trello * Monday.com * Linear Development * GitHub * GitLab * Bitbucket * Figma * Vercel ## Common authentication scenarios [Section titled “Common authentication scenarios”](#common-authentication-scenarios) ### Multi-provider authentication [Section titled “Multi-provider authentication”](#multi-provider-authentication) Enable users to connect multiple third-party accounts in your application: ```javascript 1 // Allow users to authenticate with both Gmail and Slack 2 const gmailAccount = await agentConnect.accounts.create({ 3 connection_id: gmailConnection.id, 4 identifier: 'user_123', 5 identifier_type: 'user_id' 6 }); 7 8 const slackAccount = await agentConnect.accounts.create({ 9 connection_id: slackConnection.id, 10 identifier: 'user_123', 11 identifier_type: 'user_id' 12 }); 13 14 // Generate OAuth URLs for each provider 15 const gmailAuthUrl = await agentConnect.accounts.getAuthUrl(gmailAccount.id); 16 const slackAuthUrl = await agentConnect.accounts.getAuthUrl(slackAccount.id); ``` ### Organization-level connections [Section titled “Organization-level connections”](#organization-level-connections) Authenticate once for an entire organization: ```javascript 1 // Create organization-level connection for shared access 2 const orgConnection = await agentConnect.accounts.create({ 3 connection_id: jiraConnection.id, 4 identifier: 'org_456', 5 identifier_type: 'org_id' 6 }); 7 8 // All users in the organization can access this connection 9 const authUrl = await agentConnect.accounts.getAuthUrl(orgConnection.id); ``` ### Token lifecycle management [Section titled “Token lifecycle management”](#token-lifecycle-management) Agent Auth automatically handles token refresh and expiration: ```javascript 1 // Retrieve connected account - tokens are automatically refreshed if expired 2 const account = await agentConnect.accounts.get(connectedAccount.id); 3 4 // Check authentication status 5 if (account.status === 'active') { 6 // User is authenticated and tokens are valid 7 } else if (account.status === 'expired') { 8 // Re-authentication required 9 const reAuthUrl = await agentConnect.accounts.getAuthUrl(account.id); 10 } ``` ## Key benefits [Section titled “Key benefits”](#key-benefits) ### Developer experience [Section titled “Developer experience”](#developer-experience) * **Unified authentication API**: Single interface for OAuth across all providers * **Automatic token refresh**: No manual token lifecycle management required * **Pre-built OAuth flows**: Skip complex OAuth implementation for each provider * **Multi-language SDKs**: Support for popular programming languages * **Standardized error handling**: Consistent error responses across providers ### Security and compliance [Section titled “Security and compliance”](#security-and-compliance) * **OAuth 2.0 standard**: Industry-standard authentication protocol * **Encrypted token storage**: Secure storage and transmission of access tokens * **Automatic token rotation**: Refresh tokens automatically to minimize exposure * **Audit logging**: Complete audit trail of authentication events * **SOC 2 certified**: Enterprise-grade security standards ## Authentication options [Section titled “Authentication options”](#authentication-options) Agent Auth supports multiple authentication approaches: ### Scalekit-managed OAuth [Section titled “Scalekit-managed OAuth”](#scalekit-managed-oauth) Use Scalekit’s shared OAuth applications for quick setup: * **Fast setup**: Get started in minutes without registering OAuth apps * **Shared credentials**: Pre-configured OAuth credentials for all providers * **Zero configuration**: No need to manage client IDs or secrets * **Perfect for**: Development, testing, proof of concepts ### Bring Your Own OAuth (BYOO) [Section titled “Bring Your Own OAuth (BYOO)”](#bring-your-own-oauth-byoo) Use your own OAuth applications for complete control: * **Custom branding**: Users see your application name and logo during OAuth * **Higher rate limits**: Dedicated quotas for your OAuth application * **Direct relationships**: Establish direct OAuth connections with providers * **Enhanced control**: Full control over OAuth scopes and permissions * **Perfect for**: Production applications, enterprise customers ## Getting started [Section titled “Getting started”](#getting-started) Ready to start using Agent Auth? Here’s what you need to do: [Quickstart Guide ](/agent-auth/quickstart) [Authentication Flows ](/agent-auth/authentication/auth-flows-comparison) [Providers ](/agent-auth/providers) ## Support and resources [Section titled “Support and resources”](#support-and-resources) ### Documentation [Section titled “Documentation”](#documentation) * **Authentication guides**: Step-by-step OAuth integration guides * **API reference**: Complete authentication API documentation * **SDK documentation**: Language-specific authentication examples * **Best practices**: Security and token management guidelines ### Community and support [Section titled “Community and support”](#community-and-support) * **Developer community**: Join other developers using Agent Auth * **Support portal**: Get help with authentication issues * **Professional services**: Expert assistance for complex OAuth integrations Note **Ready to get started?** Check out our [Quickstart Guide](/agent-auth/quickstart) to implement your first OAuth integration with Agent Auth in minutes. Agent Auth simplifies third-party authentication so you can focus on building features instead of managing OAuth flows. Start building today and provide seamless authentication experiences for your users. --- # DOCUMENT BOUNDARY --- # Providers > Learn about third-party application providers supported by Agent Auth and how they enable tool execution across different platforms. Providers in Agent Auth represent third-party applications that your users can connect to and interact with through Scalekit’s unified API. Each provider offers a set of tools and capabilities that can be executed on behalf of connected users. ## What are providers? [Section titled “What are providers?”](#what-are-providers) Providers are pre-configured integrations with popular third-party applications that enable your users to: * **Connect their accounts** using secure authentication methods * **Execute tools and actions** through a unified API interface * **Access data and functionality** from external applications * **Maintain secure connections** with proper authorization scopes ## Supported providers [Section titled “Supported providers”](#supported-providers) [Browse all agent connectors ](/guides/integrations/agent-connectors) Next, learn how to configure [Connections](/agent-auth/connections) for your chosen providers to enable user authentication and tool execution. --- # DOCUMENT BOUNDARY --- # Invoke tools for your AI agent > Execute tools directly, customize their behavior with modifiers, and build agentic workflows where LLMs drive tool selection. Agent Auth supports three approaches to tool calling: execute tools directly with explicit parameters, customize tool behavior with pre- and post-modifiers, or let an LLM select and invoke tools automatically based on user input. ## Direct tool execution [Section titled “Direct tool execution”](#direct-tool-execution) Using Scalekit SDK, you can execute any action on behalf of a user using the following parameters: * user context * `tool_name` * `tool_input_parameters` - Python ```python 1 # Fetch recent emails 2 tool_response = actions.execute_tool( 3 # tool input parameters 4 tool_input={ 5 'query': 'is:unread', 6 'max_results': 5 7 }, 8 # tool name to execute 9 tool_name='gmail_fetch_mails', 10 # connected_account gives the user context 11 connected_account_id=connected_account.id, 12 ) 13 14 print(f'Recent emails: {tool_response.result}') ``` - Node.js ```typescript 1 // Fetch recent emails 2 const toolResponse = await actions.executeTool({ 3 // tool name to execute 4 toolName: 'gmail_fetch_mails', 5 // connectedAccountId from a prior getOrCreateConnectedAccount call 6 connectedAccountId: 'your_connected_account_id', 7 // tool input parameters 8 toolInput: { 9 query: 'is:unread', 10 max_results: 5, 11 }, 12 }); 13 14 console.log('Recent emails:', toolResponse.result); ``` ## Customize with modifiers [Section titled “Customize with modifiers”](#customize-with-modifiers) Tool modifiers intercept and modify tool inputs and outputs using decorators. * **Pre-modifiers**: Modify tool inputs before execution * **Post-modifiers**: Modify tool outputs after execution Common uses * Reduce response size to prevent LLM context overloading * Filter emails to unread only * Add consistent parameters * Transform data formats ### Pre-modifiers [Section titled “Pre-modifiers”](#pre-modifiers) Pre-modifiers modify tool inputs before execution to enforce consistent filters, add security constraints, override LLM decisions with required behavior, or set default configurations. Example: Gmail unread filter ```python 1 from scalekit.actions.models.tool_input_output import ToolInput 2 3 # For example, we can modify the query to only fetch unread emails 4 # regardless of what the user asks for or what the LLM determines. 5 @actions.pre_modifier(tool_names=["gmail_fetch_mails"]) 6 def gmail_pre_modifier(tool_input: ToolInput): 7 tool_input['query'] = 'is:unread' 8 return tool_input ``` This modifier: * Intercepts all calls to `gmail_fetch_mails` * Forces the query to always search for unread emails only * Ensures consistent behavior regardless of user input or LLM interpretation **Multiple tools example** ```python 1 @actions.pre_modifier(tool_names=["gmail_fetch_mails", "gmail_search_emails"]) 2 def email_security_modifier(tool_input: ToolInput): 3 # Add security constraints to all email operations 4 tool_input['include_spam'] = False 5 tool_input['max_results'] = min(tool_input.get('max_results', 10), 50) 6 return tool_input ``` ### Post-modifiers [Section titled “Post-modifiers”](#post-modifiers) Post-modifiers modify tool outputs after execution to reduce token usage, transform formats for LLM consumption, extract specific data, or standardize output structure. Response format Post-modifiers must always return a dictionary with a `"response"` key: `{"response": your_data}` Example: Gmail response filtering ```python 1 from scalekit.actions.models.tool_input_output import ToolOutput 2 3 # Sometimes, the tool output needs to be modified in a deterministic way after the tool is executed. 4 # For example, we can modify the output to only return the first email snippet regardless of what the tool returns. 5 # This is an effective way to reduce the amount of data that is returned to the LLM to save on tokens. 6 @actions.post_modifier(tool_names=["gmail_fetch_mails"]) 7 def gmail_post_modifier(output: ToolOutput): 8 # Only return the first email snippet 9 # Should return a dict 10 # Response should be a dict with a key 'response' 11 for snippet in output['messages']: 12 print(f"Email snippet: {snippet['snippet']}") 13 return {"response": output['messages'][0]['snippet']} ``` This modifier: * Processes the response from `gmail_fetch_mails` * Extracts only the first email snippet instead of returning all emails * Reduces token usage by sending minimal data to the LLM ## Agentic tool calling [Section titled “Agentic tool calling”](#agentic-tool-calling) Let an LLM determine which tool to call and with what parameters based on user input. This quickstart uses LangChain to build an agent that authenticates a user with Gmail and fetches their last 5 unread emails. **Prerequisites**: Scalekit API credentials (Client ID and Client Secret) and a Python development environment. 1. ### Set up your environment [Section titled “Set up your environment”](#set-up-your-environment) Install the Scalekit Python SDK and initialize the client with your API credentials: ```sh pip install scalekit-sdk-python langchain ``` ```python import scalekit.client import os scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENV_URL"), ) actions = scalekit_client.actions ``` 2. ### Create a connected account [Section titled “Create a connected account”](#create-a-connected-account) Authorize a user to access their Gmail account by creating a connected account. This represents the user’s connection to their Gmail account: ```python 1 # Create a connected account for user if it doesn't exist already 2 response = actions.get_or_create_connected_account( 3 connection_name="gmail", 4 identifier="user_123" 5 ) 6 connected_account = response.connected_account 7 8 print(f'Connected account created: {connected_account.id}') ``` 3. ### Authenticate the user [Section titled “Authenticate the user”](#authenticate-the-user) For Scalekit to execute tools on behalf of the user, the user must grant authorization to access their Gmail account. Scalekit automatically handles the entire OAuth workflow, including token refresh. ```python 1 # If the user hasn't yet authorized the gmail connection or if the user's access token is expired, 2 # generate a magic link and redirect the user to this link so that the user can complete authorization 3 if(connected_account.status != "ACTIVE"): 4 link_response = actions.get_authorization_link( 5 connection_name="gmail", 6 identifier="user_123" 7 ) 8 print(f"🔗click on the link to authorize gmail", link_response.link) 9 input(f"⎆ Press Enter after authorizing gmail...") 10 11 # In a real app, redirect the user to this URL so that the user can complete the authentication process for their gmail account ``` 4. ### Build a LangChain agent [Section titled “Build a LangChain agent”](#build-a-langchain-agent) Build a simple agent that fetches the last 5 unread emails from the user’s inbox and generates a summary. ```python 1 from langchain_core.prompts import SystemMessagePromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate,PromptTemplate, ChatPromptTemplate 2 3 # use scalekit SDK to fetch all GMAIL tools in langchain format 4 tools = actions.langchain.get_tools( 5 identifier=identifier, 6 providers = ["GMAIL"], # all tools for provider used by default 7 page_size=100 8 ) 9 10 prompt = ChatPromptTemplate.from_messages([ 11 SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], template="You are a helpful assistant. Use tools if needed")), 12 MessagesPlaceholder(variable_name="chat_history", optional=True), 13 HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=["input"], template="{input}")), 14 MessagesPlaceholder(variable_name="agent_scratchpad") 15 ]) 16 17 llm = ChatOpenAI(model="gpt-4o") 18 agent = create_openai_tools_agent(llm, tools, prompt) 19 agent_executor = AgentExecutor(agent=agent, tools=tools,verbose=True) 20 result = agent_executor.invoke({"input":"fetch my last 5 unread emails and summarize it"} 21 ) ``` ## Next steps [Section titled “Next steps”](#next-steps) For more detailed framework-specific implementations, explore the AI framework guides: [LangChain Framework ](/agent-auth/frameworks/langchain) [Google ADK Framework ](/agent-auth/frameworks/google-adk) [OpenClaw ](/agent-auth/openclaw) --- # DOCUMENT BOUNDARY --- # Authorize a user > Learn how to authorize users for successful tool execution with Agent Auth. Scalekit provides a completely managed authentication platform to handle complex OAuth2.0, API Keys, Bearer Tokens and any other API authentication protocols required by third party applications to execute tool calls on behalf of users. This managed authentication handling enables you to build powerful agents without having to worry about handling user authentication and API authentication across different applications like Salesforce, Hubspot, GMail, Google Calendar etc. ## Authorize a user [Section titled “Authorize a user”](#authorize-a-user) If you are building an agent that needs to execute actions on behalf of a user, your agent needs to get authorization from user to give access to their application. The following code sample helps your agent complete user authorization required to make a successful authenticated tool call. * Python ```python 1 link_response = actions.get_authorization_link( 2 connection_name="gmail", # connection name to which the user needs to grant access 3 identifier="user_123" # unique user id 4 ) 5 print(f"click on the link to authorize gmail", link_response.link) 6 input(f"Press Enter after authorizing gmail...") ``` * Node.js ```typescript 1 const linkResponse = await actions.getAuthorizationLink({ 2 connectionName: 'gmail', // connection name to which the user needs to grant access 3 identifier: 'user_123', // unique user id 4 }); 5 console.log('click on the link to authorize gmail', linkResponse.link); 6 // In production, redirect the user to linkResponse.link to complete the OAuth flow ``` ## Check Authorization Status [Section titled “Check Authorization Status”](#check-authorization-status) If you would like to check whether the user has completed authorization for a given application, * Python ```python 1 response = actions.get_or_create_connected_account( 2 connection_name="gmail", 3 identifier="user_123" 4 ) 5 connected_account = response.connected_account 6 print(f"Authorization status of the connected account", connected_account.status) ``` * Node.js ```typescript 1 const response = await actions.getOrCreateConnectedAccount({ 2 connectionName: 'gmail', 3 identifier: 'user_123', 4 }); 5 const connectedAccount = response.connectedAccount; 6 console.log('Authorization status of the connected account', connectedAccount?.status); ``` --- # DOCUMENT BOUNDARY --- # Pre and Post Processors > Learn how to create pre and post processor workflows that are run before or after tool execution with Agent Auth. Custom pre and post processors are a way to create custom workflows that are run before or after tool execution with Agent Auth. They are useful for: * Validating and transforming input data * Processing and Formatting output data * Adding additional context to the tool execution ## Usage [Section titled “Usage”](#usage) --- # DOCUMENT BOUNDARY --- # Custom Tools > Learn how to create custom tools with Agent Auth. # Custom Tools [Section titled “Custom Tools”](#custom-tools) Custom tools are a way to create custom tools that can be used in Agent Auth. ## Usage [Section titled “Usage”](#usage) --- # DOCUMENT BOUNDARY --- # Tools Overview > Learn about tools in Agent Auth - the standardized functions that enable you to perform actions across different third-party providers. LLMs today are very powerful reasoning and answering machines but their ability is restricted to data sets that they are trained upon and cannot natively interact with web services or saas applications. Tool Calling or Function Calling is how you extend the capabilities of these models to interact and take actions in third party applications on behalf of the users. For example, if you would like to build an email summarizer agent, there are a few challenges that you need to tackle: 1. How to give agents access to gmail 2. How to authorize these agents access to my gmail account 3. What should be the appropriate input parameters to access gmail based on user context and query Agent Auth product solves these problems by giving you simple abstractions using our SDK to help you give additional capabilities to the agents you are building regardless of the underlying model and agent framework in three simple steps. 1. Use Scalekit SDK to fetch all the appropriate tools 2. Complete user authorization handling in one single line of code 3. Use Scalekit’s optimized tool metadata and pass it to the underlying model for optimal tool selection and input parameters. ## Tool Metadata [Section titled “Tool Metadata”](#tool-metadata) Every tool in Agent Auth follows a consistent structure with a name, description and structured input and output schema. Agentic frameworks like Langchain can work with the underlying LLMs to select the right tool to solve the user’s query based on the tool metadata. ### Sample Tool definition [Section titled “Sample Tool definition”](#sample-tool-definition) ```json 1 { 2 "name": "gmail_send_email", 3 "display_name": "Send Email", 4 "description": "Send an email message to one or more recipients", 5 "provider": "gmail", 6 "category": "communication", 7 "input_schema": { 8 "type": "object", 9 "properties": { 10 "to": { 11 "type": "array", 12 "items": {"type": "string", "format": "email"}, 13 "description": "Email addresses of recipients" 14 }, 15 "subject": { 16 "type": "string", 17 "description": "Email subject line" 18 }, 19 "body": { 20 "type": "string", 21 "description": "Email body content" 22 } 23 }, 24 "required": ["to", "subject", "body"] 25 }, 26 "output_schema": { 27 "type": "object", 28 "properties": { 29 "message_id": { 30 "type": "string", 31 "description": "Unique identifier for the sent message" 32 }, 33 "status": { 34 "type": "string", 35 "enum": ["sent", "queued", "failed"], 36 "description": "Status of the email sending operation" 37 } 38 } 39 } 40 } ``` ## Best practices [Section titled “Best practices”](#best-practices) 1. **Tool Selection:** Even though tools provide additional capabilities to the agents, the real challenge in leveraging underlying LLMs capability to select the right tool to solve the job at hand. And LLMs do a poor job when you throw all the available tools you have at your disposal and ask LLMs to pick the right tool. So, be sure to limit the number of tools that you provide in the context to the LLM so that they do a good job in tool selection and filling in the appropriate input parameters to actually execute a certain action successfully. 2. **Add deterministic overrides in undeterministic workflows:** Because LLMs are unpredictable super machines, do not trust them to reliably execute the same workflow every single time in the exact same manner. If your agent has some deterministic patterns or workflows, use the pre-execution modifiers to always set exact input parameters for a given tool. For example, if your agent always reads only unread emails, create a pre-execution modifier to add `is:unread` to the query input param while fetching emails using gmail\_fetch\_emails tool. 3. **Context Window Awareness:** Similar to the point above, always be conscious of overloading context window of the underlying models. Don’t send the entire tool execution response/output to the underlying model for processing the execution response. Use the post-execution modifiers to select only the required and necessary fields in the tool output response before sending the data to the LLMs. *** Tools are the fundamental building blocks through which you can give real world capabilities for the agents you are building. By understanding how to use them effectively, you can build sophisticated agents that seamlessly connect your application to the tools your users already love. --- # DOCUMENT BOUNDARY --- # Proxy Tools > Learn how to make direct API calls to providers using Agent Auth's proxy tools. Custom tool definitions allow you to create specialized tools tailored to your specific business needs. You can combine multiple provider tools, add custom logic, and create reusable workflows that go beyond standard tool functionality. ## What are custom tools? [Section titled “What are custom tools?”](#what-are-custom-tools) Custom tools are user-defined functions that: * **Extend existing tools**: Build on top of standard provider tools * **Combine multiple operations**: Create workflows that use multiple tools * **Add business logic**: Include custom validation, processing, and formatting * **Create reusable patterns**: Standardize common operations across your team * **Integrate with external systems**: Connect to your own APIs and services ## Custom tool structure [Section titled “Custom tool structure”](#custom-tool-structure) Every custom tool follows a standardized structure: ```javascript 1 { 2 name: 'custom_tool_name', 3 display_name: 'Custom Tool Display Name', 4 description: 'Description of what the tool does', 5 category: 'custom', 6 provider: 'custom', 7 input_schema: { 8 type: 'object', 9 properties: { 10 // Define input parameters 11 }, 12 required: ['required_param'] 13 }, 14 output_schema: { 15 type: 'object', 16 properties: { 17 // Define output format 18 } 19 }, 20 implementation: async (parameters, context) => { 21 // Custom tool logic 22 return result; 23 } 24 } ``` ## Creating custom tools [Section titled “Creating custom tools”](#creating-custom-tools) ### Basic custom tool [Section titled “Basic custom tool”](#basic-custom-tool) Here’s a simple custom tool that sends a welcome email: ```javascript 1 const sendWelcomeEmail = { 2 name: 'send_welcome_email', 3 display_name: 'Send Welcome Email', 4 description: 'Send a personalized welcome email to new users', 5 category: 'communication', 6 provider: 'custom', 7 input_schema: { 8 type: 'object', 9 properties: { 10 user_name: { 11 type: 'string', 12 description: 'Name of the new user' 13 }, 14 user_email: { 15 type: 'string', 16 format: 'email', 17 description: 'Email address of the new user' 18 }, 19 company_name: { 20 type: 'string', 21 description: 'Name of the company' 22 } 23 }, 24 required: ['user_name', 'user_email', 'company_name'] 25 }, 26 output_schema: { 27 type: 'object', 28 properties: { 29 message_id: { 30 type: 'string', 31 description: 'ID of the sent email' 32 }, 33 status: { 34 type: 'string', 35 enum: ['sent', 'failed'], 36 description: 'Status of the email' 37 } 38 } 39 }, 40 implementation: async (parameters, context) => { 41 const { user_name, user_email, company_name } = parameters; 42 43 // Generate personalized email content 44 const emailBody = ` 45 Welcome to ${company_name}, ${user_name}! 46 47 We're excited to have you join our team. Here are some next steps: 48 49 1. Complete your profile setup 50 2. Join our Slack workspace 51 3. Schedule a meeting with your manager 52 53 If you have any questions, don't hesitate to reach out! 54 55 Best regards, 56 The ${company_name} Team 57 `; 58 59 // Send email using standard email tool 60 const result = await context.tools.execute({ 61 tool: 'send_email', 62 parameters: { 63 to: [user_email], 64 subject: `Welcome to ${company_name}!`, 65 body: emailBody 66 } 67 }); 68 69 return { 70 message_id: result.message_id, 71 status: result.status === 'sent' ? 'sent' : 'failed' 72 }; 73 } 74 }; ``` ### Multi-step workflow tool [Section titled “Multi-step workflow tool”](#multi-step-workflow-tool) Create a tool that combines multiple operations: ```javascript 1 const createProjectWorkflow = { 2 name: 'create_project_workflow', 3 display_name: 'Create Project Workflow', 4 description: 'Create a complete project setup with Jira project, Slack channel, and team notifications', 5 category: 'project_management', 6 provider: 'custom', 7 input_schema: { 8 type: 'object', 9 properties: { 10 project_name: { 11 type: 'string', 12 description: 'Name of the project' 13 }, 14 project_key: { 15 type: 'string', 16 description: 'Project key for Jira' 17 }, 18 team_members: { 19 type: 'array', 20 items: { type: 'string', format: 'email' }, 21 description: 'Team member email addresses' 22 }, 23 project_description: { 24 type: 'string', 25 description: 'Project description' 26 } 27 }, 28 required: ['project_name', 'project_key', 'team_members'] 29 }, 30 output_schema: { 31 type: 'object', 32 properties: { 33 jira_project_id: { type: 'string' }, 34 slack_channel_id: { type: 'string' }, 35 notifications_sent: { type: 'number' } 36 } 37 }, 38 implementation: async (parameters, context) => { 39 const { project_name, project_key, team_members, project_description } = parameters; 40 41 try { 42 // Step 1: Create Jira project 43 const jiraProject = await context.tools.execute({ 44 tool: 'create_jira_project', 45 parameters: { 46 key: project_key, 47 name: project_name, 48 description: project_description, 49 project_type: 'software' 50 } 51 }); 52 53 // Step 2: Create Slack channel 54 const slackChannel = await context.tools.execute({ 55 tool: 'create_channel', 56 parameters: { 57 name: `${project_key.toLowerCase()}-team`, 58 topic: `Discussion for ${project_name}`, 59 is_private: false 60 } 61 }); 62 63 // Step 3: Send notifications to team members 64 let notificationCount = 0; 65 for (const member of team_members) { 66 try { 67 await context.tools.execute({ 68 tool: 'send_email', 69 parameters: { 70 to: [member], 71 subject: `New Project: ${project_name}`, 72 body: ` 73 You've been added to the new project "${project_name}". 74 75 Jira Project: ${jiraProject.project_url} 76 Slack Channel: #${slackChannel.channel_name} 77 78 Please join the Slack channel to start collaborating! 79 ` 80 } 81 }); 82 notificationCount++; 83 } catch (error) { 84 console.error(`Failed to send notification to ${member}:`, error); 85 } 86 } 87 88 // Step 4: Post welcome message to Slack channel 89 await context.tools.execute({ 90 tool: 'send_message', 91 parameters: { 92 channel: `#${slackChannel.channel_name}`, 93 text: `<� Welcome to ${project_name}! This channel is for project discussion and updates.` 94 } 95 }); 96 97 return { 98 jira_project_id: jiraProject.project_id, 99 slack_channel_id: slackChannel.channel_id, 100 notifications_sent: notificationCount 101 }; 102 103 } catch (error) { 104 throw new Error(`Project creation failed: ${error.message}`); 105 } 106 } 107 }; ``` ### Data processing tool [Section titled “Data processing tool”](#data-processing-tool) Create a tool that processes and analyzes data: ```javascript 1 const generateTeamReport = { 2 name: 'generate_team_report', 3 display_name: 'Generate Team Report', 4 description: 'Generate a comprehensive team performance report from multiple sources', 5 category: 'analytics', 6 provider: 'custom', 7 input_schema: { 8 type: 'object', 9 properties: { 10 team_members: { 11 type: 'array', 12 items: { type: 'string', format: 'email' }, 13 description: 'Team member email addresses' 14 }, 15 start_date: { 16 type: 'string', 17 format: 'date', 18 description: 'Report start date' 19 }, 20 end_date: { 21 type: 'string', 22 format: 'date', 23 description: 'Report end date' 24 }, 25 include_calendar: { 26 type: 'boolean', 27 default: true, 28 description: 'Include calendar analysis' 29 } 30 }, 31 required: ['team_members', 'start_date', 'end_date'] 32 }, 33 output_schema: { 34 type: 'object', 35 properties: { 36 report_url: { type: 'string' }, 37 summary: { type: 'object' }, 38 sent_to: { type: 'array', items: { type: 'string' } } 39 } 40 }, 41 implementation: async (parameters, context) => { 42 const { team_members, start_date, end_date, include_calendar } = parameters; 43 44 // Fetch Jira issues assigned to team members 45 const jiraIssues = await context.tools.execute({ 46 tool: 'fetch_issues', 47 parameters: { 48 jql: `assignee in (${team_members.join(',')}) AND created >= ${start_date} AND created <= ${end_date}`, 49 fields: ['summary', 'status', 'assignee', 'created', 'resolved'] 50 } 51 }); 52 53 // Fetch calendar events if requested 54 let calendarData = null; 55 if (include_calendar) { 56 calendarData = await context.tools.execute({ 57 tool: 'fetch_events', 58 parameters: { 59 start_date: start_date, 60 end_date: end_date, 61 attendees: team_members 62 } 63 }); 64 } 65 66 // Process and analyze data 67 const report = { 68 period: { start_date, end_date }, 69 team_size: team_members.length, 70 issues: { 71 total: jiraIssues.issues.length, 72 completed: jiraIssues.issues.filter(i => i.status === 'Done').length, 73 in_progress: jiraIssues.issues.filter(i => i.status === 'In Progress').length 74 }, 75 meetings: calendarData ? { 76 total: calendarData.events.length, 77 hours: calendarData.events.reduce((acc, event) => acc + event.duration, 0) 78 } : null 79 }; 80 81 // Generate HTML report 82 const htmlReport = ` 83 84 Team Report - ${start_date} to ${end_date} 85 86

Team Performance Report

87

Summary

88

Team Size: ${report.team_size}

89

Total Issues: ${report.issues.total}

90

Completed Issues: ${report.issues.completed}

91

In Progress: ${report.issues.in_progress}

92 ${report.meetings ? `

Total Meetings: ${report.meetings.total}

` : ''} 93 94 95 `; 96 97 // Send report via email 98 const emailResults = await Promise.all( 99 team_members.map(member => 100 context.tools.execute({ 101 tool: 'send_email', 102 parameters: { 103 to: [member], 104 subject: `Team Report - ${start_date} to ${end_date}`, 105 html_body: htmlReport 106 } 107 }) 108 ) 109 ); 110 111 return { 112 report_url: 'Generated and sent via email', 113 summary: report, 114 sent_to: team_members.filter((_, index) => emailResults[index].status === 'sent') 115 }; 116 } 117 }; ``` ## Registering custom tools [Section titled “Registering custom tools”](#registering-custom-tools) ### Using the API [Section titled “Using the API”](#using-the-api) Register your custom tools with Agent Auth: * JavaScript ```javascript 1 // Register a custom tool 2 const registeredTool = await agentConnect.tools.register({ 3 ...sendWelcomeEmail, 4 organization_id: 'your_org_id' 5 }); 6 7 console.log('Tool registered:', registeredTool.id); ``` * Python ```python 1 # Register a custom tool 2 registered_tool = agent_connect.tools.register( 3 **send_welcome_email, 4 organization_id='your_org_id' 5 ) 6 7 print(f'Tool registered: {registered_tool.id}') ``` * cURL ```bash 1 curl -X POST "${SCALEKIT_BASE_URL}/v1/connect/tools/custom" \ 2 -H "Authorization: Bearer ${SCALEKIT_CLIENT_SECRET}" \ 3 -H "Content-Type: application/json" \ 4 -d '{ 5 "name": "send_welcome_email", 6 "display_name": "Send Welcome Email", 7 "description": "Send a personalized welcome email to new users", 8 "category": "communication", 9 "provider": "custom", 10 "input_schema": {...}, 11 "output_schema": {...}, 12 "implementation": "async (parameters, context) => {...}" 13 }' ``` ### Using the dashboard [Section titled “Using the dashboard”](#using-the-dashboard) 1. Navigate to **Tools** in your Agent Auth dashboard 2. Click **Create Custom Tool** 3. Fill in the tool definition form 4. Test the tool with sample parameters 5. Save and activate the tool ## Tool context and utilities [Section titled “Tool context and utilities”](#tool-context-and-utilities) The `context` object provides access to: ### Standard tools [Section titled “Standard tools”](#standard-tools) Execute any standard Agent Auth tool: ```javascript 1 // Execute standard tools 2 const result = await context.tools.execute({ 3 tool: 'send_email', 4 parameters: { ... } 5 }); 6 7 // Execute with specific connected account 8 const result = await context.tools.execute({ 9 connected_account_id: 'specific_account', 10 tool: 'send_email', 11 parameters: { ... } 12 }); ``` ### Connected accounts [Section titled “Connected accounts”](#connected-accounts) Access connected account information: ```javascript 1 // Get connected account details 2 const account = await context.accounts.get(accountId); 3 4 // List accounts for a user 5 const accounts = await context.accounts.list({ 6 identifier: 'user_123', 7 provider: 'gmail' 8 }); ``` ### Utilities [Section titled “Utilities”](#utilities) Access utility functions: ```javascript 1 // Generate unique IDs 2 const id = context.utils.generateId(); 3 4 // Format dates 5 const formatted = context.utils.formatDate(date, 'YYYY-MM-DD'); 6 7 // Validate email 8 const isValid = context.utils.isValidEmail(email); 9 10 // HTTP requests 11 const response = await context.utils.httpRequest({ 12 url: 'https://api.example.com/data', 13 method: 'GET', 14 headers: { 'Authorization': 'Bearer token' } 15 }); ``` ### Error handling [Section titled “Error handling”](#error-handling) Throw structured errors: ```javascript 1 // Throw validation error 2 throw new context.errors.ValidationError('Invalid email format'); 3 4 // Throw business logic error 5 throw new context.errors.BusinessLogicError('User not found'); 6 7 // Throw external API error 8 throw new context.errors.ExternalAPIError('GitHub API returned 500'); ``` ## Testing custom tools [Section titled “Testing custom tools”](#testing-custom-tools) ### Unit testing [Section titled “Unit testing”](#unit-testing) Test custom tools in isolation: ```javascript 1 // Mock context for testing 2 const mockContext = { 3 tools: { 4 execute: jest.fn().mockResolvedValue({ 5 message_id: 'test_msg_123', 6 status: 'sent' 7 }) 8 }, 9 utils: { 10 generateId: () => 'test_id_123', 11 formatDate: (date, format) => '2024-01-15' 12 } 13 }; 14 15 // Test custom tool 16 const result = await sendWelcomeEmail.implementation({ 17 user_name: 'John Doe', 18 user_email: 'john@example.com', 19 company_name: 'Acme Corp' 20 }, mockContext); 21 22 expect(result.status).toBe('sent'); 23 expect(mockContext.tools.execute).toHaveBeenCalledWith({ 24 tool: 'send_email', 25 parameters: expect.objectContaining({ 26 to: ['john@example.com'], 27 subject: 'Welcome to Acme Corp!' 28 }) 29 }); ``` ### Integration testing [Section titled “Integration testing”](#integration-testing) Test with real Agent Auth: ```javascript 1 // Test custom tool with real connections 2 const testResult = await agentConnect.tools.execute({ 3 connected_account_id: 'test_gmail_account', 4 tool: 'send_welcome_email', 5 parameters: { 6 user_name: 'Test User', 7 user_email: 'test@example.com', 8 company_name: 'Test Company' 9 } 10 }); 11 12 console.log('Test result:', testResult); ``` ## Best practices [Section titled “Best practices”](#best-practices) ### Tool design [Section titled “Tool design”](#tool-design) * **Single responsibility**: Each tool should have a clear, single purpose * **Consistent naming**: Use descriptive, consistent naming conventions * **Clear documentation**: Provide detailed descriptions and examples * **Error handling**: Implement comprehensive error handling * **Input validation**: Validate all input parameters ### Performance optimization [Section titled “Performance optimization”](#performance-optimization) * **Parallel execution**: Use Promise.all() for independent operations * **Caching**: Cache frequently accessed data * **Batch operations**: Group similar operations together * **Timeout handling**: Set appropriate timeouts for external calls ### Security considerations [Section titled “Security considerations”](#security-considerations) * **Input sanitization**: Sanitize all user inputs * **Permission checks**: Verify user permissions before execution * **Sensitive data**: Handle sensitive data securely * **Rate limiting**: Implement rate limiting for resource-intensive operations ## Custom tool examples [Section titled “Custom tool examples”](#custom-tool-examples) ### Slack notification tool [Section titled “Slack notification tool”](#slack-notification-tool) ```javascript 1 const sendSlackNotification = { 2 name: 'send_slack_notification', 3 display_name: 'Send Slack Notification', 4 description: 'Send formatted notifications to Slack with optional mentions', 5 category: 'communication', 6 provider: 'custom', 7 input_schema: { 8 type: 'object', 9 properties: { 10 channel: { type: 'string' }, 11 message: { type: 'string' }, 12 severity: { type: 'string', enum: ['info', 'warning', 'error'] }, 13 mentions: { type: 'array', items: { type: 'string' } } 14 }, 15 required: ['channel', 'message'] 16 }, 17 output_schema: { 18 type: 'object', 19 properties: { 20 message_ts: { type: 'string' }, 21 permalink: { type: 'string' } 22 } 23 }, 24 implementation: async (parameters, context) => { 25 const { channel, message, severity = 'info', mentions = [] } = parameters; 26 27 const colors = { 28 info: 'good', 29 warning: 'warning', 30 error: 'danger' 31 }; 32 33 const mentionText = mentions.length > 0 ? 34 `${mentions.map(m => `<@${m}>`).join(' ')} ` : ''; 35 36 return await context.tools.execute({ 37 tool: 'send_message', 38 parameters: { 39 channel, 40 text: `${mentionText}${message}`, 41 attachments: [ 42 { 43 color: colors[severity], 44 text: message, 45 ts: Math.floor(Date.now() / 1000) 46 } 47 ] 48 } 49 }); 50 } 51 }; ``` ### Calendar scheduling tool [Section titled “Calendar scheduling tool”](#calendar-scheduling-tool) ```javascript 1 const scheduleTeamMeeting = { 2 name: 'schedule_team_meeting', 3 display_name: 'Schedule Team Meeting', 4 description: 'Find available time slots and schedule team meetings', 5 category: 'scheduling', 6 provider: 'custom', 7 input_schema: { 8 type: 'object', 9 properties: { 10 attendees: { type: 'array', items: { type: 'string' } }, 11 duration: { type: 'number', minimum: 15 }, 12 preferred_times: { type: 'array', items: { type: 'string' } }, 13 meeting_title: { type: 'string' }, 14 meeting_description: { type: 'string' } 15 }, 16 required: ['attendees', 'duration', 'meeting_title'] 17 }, 18 output_schema: { 19 type: 'object', 20 properties: { 21 event_id: { type: 'string' }, 22 scheduled_time: { type: 'string' }, 23 attendees_notified: { type: 'number' } 24 } 25 }, 26 implementation: async (parameters, context) => { 27 const { attendees, duration, preferred_times, meeting_title, meeting_description } = parameters; 28 29 // Find available time slots 30 const availableSlots = await context.tools.execute({ 31 tool: 'find_available_slots', 32 parameters: { 33 attendees, 34 duration, 35 preferred_times: preferred_times || [] 36 } 37 }); 38 39 if (availableSlots.length === 0) { 40 throw new context.errors.BusinessLogicError('No available time slots found'); 41 } 42 43 // Schedule the meeting at the first available slot 44 const selectedSlot = availableSlots[0]; 45 const event = await context.tools.execute({ 46 tool: 'create_event', 47 parameters: { 48 title: meeting_title, 49 description: meeting_description, 50 start_time: selectedSlot.start_time, 51 end_time: selectedSlot.end_time, 52 attendees 53 } 54 }); 55 56 return { 57 event_id: event.event_id, 58 scheduled_time: selectedSlot.start_time, 59 attendees_notified: attendees.length 60 }; 61 } 62 }; ``` ## Versioning and deployment [Section titled “Versioning and deployment”](#versioning-and-deployment) ### Version management [Section titled “Version management”](#version-management) Version your custom tools for backward compatibility: ```javascript 1 const toolV2 = { 2 ...originalTool, 3 version: '2.0.0', 4 // Updated implementation 5 }; 6 7 // Deploy new version 8 await agentConnect.tools.register(toolV2); 9 10 // Deprecate old version 11 await agentConnect.tools.deprecate(originalTool.name, '1.0.0'); ``` ### Deployment strategies [Section titled “Deployment strategies”](#deployment-strategies) * **Blue-green deployment**: Deploy new version alongside old version * **Canary deployment**: Gradually roll out to subset of users * **Feature flags**: Use feature flags to control tool availability * **Rollback strategy**: Plan for quick rollback if issues arise Note **Ready to build?** Start with simple custom tools and gradually add complexity. Test thoroughly before deploying to production, and consider the impact on your users when making changes. Custom tools unlock the full potential of Agent Auth by allowing you to create specialized workflows that perfectly match your business needs. With proper design, testing, and deployment practices, you can build powerful tools that enhance your team’s productivity and streamline complex operations. --- # 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/auth-methods/enterprise-sso/#identify-and-enforce-sso-for-organization-users). 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. *** ## 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 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. **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. ![](/.netlify/images?url=_astro%2Forganization_domain.CYaGBzer.png\&w=2940\&h=1592\&dpl=69cce21a4f77360008b1503a) 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.bPxTL5wR.png\&w=3026\&h=974\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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. #### 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=69cce21a4f77360008b1503a) 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** ![](/.netlify/images?url=_astro%2F1.C37ffu3h.png\&w=2221\&h=1207\&dpl=69cce21a4f77360008b1503a) 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) 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. ![](/.netlify/images?url=_astro%2Ftest-connection.8nGwOF1-.png\&w=2468\&h=1374\&dpl=69cce21a4f77360008b1503a) ## 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 ![](/.netlify/images?url=_astro%2Ffull-page-highlighth-defaults.Cs9-9nAm.png\&w=3098\&h=1896\&dpl=69cce21a4f77360008b1503a) 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. ![](/.netlify/images?url=_astro%2Fdefault_org_member_role.DzatyaVW.png\&w=2932\&h=1588\&dpl=69cce21a4f77360008b1503a) ## 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 1 // Decode JWT and check admin permissions 2 const decodedToken = decodeJWT(adminAccessToken); 3 4 // Check if user has admin role or required permissions 5 const isAdmin = decodedToken.roles.includes('admin'); 6 const hasPermission = decodedToken.permissions?.includes('users.write') || 7 decodedToken.permissions?.includes('roles.assign'); 8 9 if (!isAdmin && !hasPermission) { 10 throw new Error('Insufficient permissions to assign roles'); 11 } ``` * Python Verify permissions ```python 1 # Decode JWT and check admin permissions 2 decoded_token = decode_jwt(access_token) 3 4 # Check if user has admin role or required permissions 5 is_admin = 'admin' in decoded_token.get('roles', []) 6 has_permission = any(perm in decoded_token.get('permissions', []) 7 for perm in ['users.write', 'roles.assign']) 8 9 if not is_admin and not has_permission: 10 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 1 // Structure and validate role assignment data 2 const roleAssignmentData = { 3 user_id: targetUserId, 4 organization_id: targetOrgId, 5 roles: newRoles, 6 // Additional metadata for auditing 7 performed_by: decodedToken.sub, 8 timestamp: new Date().toISOString() 9 }; 10 11 // Validate required fields 12 if (!roleAssignmentData.user_id || !roleAssignmentData.organization_id || !roleAssignmentData.roles) { 13 throw new Error('Missing required identifiers for role assignment'); 14 } ``` - Python Collect and validate identifiers ```python 1 # Structure and validate role assignment data 2 role_assignment_data = { 3 'user_id': target_user_id, 4 'organization_id': target_org_id, 5 'roles': new_roles, 6 # Additional metadata for auditing 7 'performed_by': decoded_token.get('sub'), 8 'timestamp': datetime.utcnow().isoformat() 9 } 10 11 # Validate required fields 12 if not all([role_assignment_data['user_id'], 13 role_assignment_data['organization_id'], 14 role_assignment_data['roles']]): 15 raise ValueError("Missing required identifiers for role assignment") ``` - Go Collect and validate identifiers ```go 1 // Structure and validate role assignment data 2 roleAssignmentData := map[string]interface{}{ 3 "user_id": req.UserID, 4 "organization_id": req.OrganizationID, 5 "roles": req.Roles, 6 // Additional metadata for auditing 7 "performed_by": decodedToken["sub"], 8 "timestamp": time.Now().UTC().Format(time.RFC3339), 9 } 10 11 // Validate required fields 12 if req.UserID == "" || req.OrganizationID == "" || len(req.Roles) == 0 { 13 return ValidationResult{Success: false, Error: "Missing required identifiers"} 14 } ``` - Java Collect and validate identifiers ```java 1 // Structure and validate role assignment data 2 Map roleAssignmentData = new HashMap<>(); 3 roleAssignmentData.put("user_id", request.userId); 4 roleAssignmentData.put("organization_id", request.organizationId); 5 roleAssignmentData.put("roles", request.roles); 6 7 // Additional metadata for auditing 8 roleAssignmentData.put("performed_by", decodedToken.getSubject()); 9 roleAssignmentData.put("timestamp", Instant.now().toString()); 10 11 // Validate required fields 12 if (request.userId == null || request.organizationId == null || request.roles == null) { 13 throw new IllegalArgumentException("Missing required identifiers for role assignment"); 14 } ``` 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 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. ![](/.netlify/images?url=_astro%2Fapp-roles-view.CxtYSlHh.png\&w=3026\&h=1802\&dpl=69cce21a4f77360008b1503a) 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. ![](/.netlify/images?url=_astro%2Fadd-organization-role.D9e4-Diz.png\&w=2934\&h=1586\&dpl=69cce21a4f77360008b1503a) 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 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 --- # 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. 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 --- # 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. This guide shows you how to 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=69cce21a4f77360008b1503a) 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 (which points to Scalekit’s `/oidc/logout` endpoint), clearing session cookies, and redirecting to Scalekit. * Node.js Express.js ```javascript 1 app.get('/logout', (req, res) => { 2 // Step 1: Extract the ID token (needed for Scalekit logout) 3 const idTokenHint = req.cookies.idToken; 4 const postLogoutRedirectUri = 'http://localhost:3000/login'; 5 6 // Step 2: Generate the Scalekit logout URL (points to /oidc/logout endpoint) 7 const logoutUrl = scalekit.getLogoutUrl( 8 idTokenHint, // ID token to invalidate 9 postLogoutRedirectUri // URL that scalekit redirects after session invalidation 10 ); 11 12 // Step 3: Clear all session cookies 13 res.clearCookie('accessToken'); 14 res.clearCookie('refreshToken'); 15 res.clearCookie('idToken'); // Clear AFTER using it for logout URL 16 17 // Step 4: Redirect to Scalekit to invalidate the session 18 res.redirect(logoutUrl); 19 }); ``` * Python Flask ```python 1 from flask import request, redirect, make_response 2 from scalekit import LogoutUrlOptions 3 4 @app.route('/logout') 5 def logout(): 6 # Step 1: Extract the ID token (needed for Scalekit logout) 7 id_token = request.cookies.get('idToken') 8 post_logout_redirect_uri = 'http://localhost:3000/login' 9 10 # Step 2: Generate the Scalekit logout URL (points to /oidc/logout endpoint) 11 logout_url = scalekit_client.get_logout_url( 12 LogoutUrlOptions( 13 id_token_hint=id_token, 14 post_logout_redirect_uri=post_logout_redirect_uri 15 ) 16 ) 17 18 # Step 3: Create response and clear all session cookies 19 response = make_response(redirect(logout_url)) 20 response.set_cookie('accessToken', '', max_age=0) 21 response.set_cookie('refreshToken', '', max_age=0) 22 response.set_cookie('idToken', '', max_age=0) # Clear AFTER using it for logout URL 23 24 # Step 4: Return response that redirects to Scalekit 25 return response ``` * Go 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 } ``` * Java 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 } ``` The logout flow clears cookies **AFTER** extracting the ID token and generating the logout URL. This ensures the ID token is available for Scalekit’s logout endpoint. 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 * Extract the ID token BEFORE clearing cookies (needed for Scalekit logout) * Clear all session cookies from your application * Redirect to Scalekit’s logout endpoint to invalidate the session server-side * Ensure your post-logout redirect URI is registered in the Scalekit dashboard ## Common logout scenarios [Section titled “Common logout scenarios”](#common-logout-scenarios) Which endpoint should I use for logout? Use `/oidc/logout` (end\_session\_endpoint) for user logout functionality. This endpoint requires a browser redirect and clears the user’s session server-side. 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 1 fetch('https://your-env.scalekit.dev/oidc/logout', { 2 method: 'POST', 3 body: JSON.stringify({ id_token_hint: idToken }) 4 }); 5 // Session cookie is NOT included, Scalekit can't identify the session ``` **✅ Works - Browser redirect:** ```javascript 1 const logoutUrl = scalekit.getLogoutUrl(idToken, postLogoutRedirectUri); 2 window.location.href = logoutUrl; 3 // 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. This guide shows you how to store these tokens securely with encryption and proper cookie attributes, validate them on every request, and refresh them transparently in middleware to maintain seamless user sessions. 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=69cce21a4f77360008b1503a) 1. ## Store session tokens securely [Section titled “Store session tokens securely”](#store-session-tokens-securely) After successful identity verification using any of the auth methods (Magic Link & OTP, social, enterprise SSO), your application receives session tokens(access and refresh tokens) towards the [end of the login](/authenticate/fsa/complete-login/). * 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 } ``` Request offline\_access to receive a refresh token A refresh token is only included in the authentication response when you include the `offline_access` scope in your authorization URL. If your authorization URL does not include `offline_access`, `authResult.refreshToken` will be `null` or undefined. Always include `offline_access` alongside `openid`, `profile`, and `email` when building your authorization URL: ```js 1 scopes: ['openid', 'profile', 'email', 'offline_access'] ``` Additionally, Scalekit **rotates refresh tokens** — every time you use a refresh token to get a new access token, you receive a new refresh token. Store the new refresh token immediately and discard the old one. Replaying a used refresh token will result in an error. Store each token based on its security requirements. For SPAs and mobile apps, consider storing access tokens in memory and sending via `Authorization: Bearer` headers to minimize CSRF exposure. For traditional web apps, use the cookie-based approach below: * **Access Token**: Store in a secure, HttpOnly cookie with proper `Path` scoping (e.g., `/api`) to prevent XSS attacks. This token has a short lifespan and provides access to protected resources. * **Refresh Token**: Store in a separate HttpOnly, Secure cookie with `Path=/auth/refresh` scoping. This limits the refresh token to only be sent to your refresh endpoint, reducing exposure. Rotate the token on each use to detect theft. * **ID Token**: Ensure it is stored in local storage or a cookie so that it remains accessible at runtime, which is necessary for logging the user out successfully. - Node.js Express.js ```javascript 4 collapsed lines 1 import cookieParser from 'cookie-parser'; 2 // Enable parsing of cookies from request headers 3 app.use(cookieParser()); 4 5 // Extract authentication data from the successful authentication response 6 const { accessToken, expiresIn, refreshToken, user } = authResult; 7 8 // Encrypt tokens before storing to add an additional security layer 9 const encryptedAccessToken = encrypt(accessToken); 10 const encryptedRefreshToken = encrypt(refreshToken); 11 12 // Store encrypted access token in HttpOnly cookie 13 res.cookie('accessToken', encryptedAccessToken, { 14 maxAge: (expiresIn - 60) * 1000, // Subtract 60s buffer for clock skew (milliseconds) 15 httpOnly: true, // Prevents JavaScript access to mitigate XSS attacks 16 secure: process.env.NODE_ENV === 'production', // HTTPS-only in production 17 sameSite: 'strict' // Prevents CSRF attacks 18 }); 19 20 // Store encrypted refresh token in separate HttpOnly cookie 21 res.cookie('refreshToken', encryptedRefreshToken, { 22 httpOnly: true, // Prevents JavaScript access to mitigate XSS attacks 23 secure: process.env.NODE_ENV === 'production', // HTTPS-only in production 24 sameSite: 'strict' // Prevents CSRF attacks 25 }); ``` - Python Flask ```python 4 collapsed lines 1 from flask import Flask, make_response, request 2 import os 3 app = Flask(__name__) 4 5 # Extract authentication data from the successful authentication response 6 access_token = auth_result.access_token 7 expires_in = auth_result.expires_in 8 refresh_token = auth_result.refresh_token 9 user = auth_result.user 10 11 # Encrypt tokens before storing to add an additional security layer 12 encrypted_access_token = encrypt(access_token) 13 encrypted_refresh_token = encrypt(refresh_token) 14 15 response = make_response() 16 17 # Store encrypted access token in HttpOnly cookie 18 response.set_cookie( 19 'accessToken', 20 encrypted_access_token, 21 max_age=expires_in - 60, # Subtract 60s buffer for clock skew (seconds in Flask) 22 httponly=True, # Prevents JavaScript access to mitigate XSS attacks 23 secure=os.environ.get('FLASK_ENV') == 'production', # HTTPS-only in production 24 samesite='Strict' # Prevents CSRF attacks 25 ) 26 27 # Store encrypted refresh token in separate HttpOnly cookie 28 response.set_cookie( 29 'refreshToken', 30 encrypted_refresh_token, 31 httponly=True, # Prevents JavaScript access to mitigate XSS attacks 32 secure=os.environ.get('FLASK_ENV') == 'production', # HTTPS-only in production 33 samesite='Strict' # Prevents CSRF attacks 34 ) ``` - Go 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 ) ``` - Java 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 access token before handling requests [Section titled “Check the access token before handling requests”](#check-the-access-token-before-handling-requests) Validate every request for a valid access token in your application. Create middleware to protect your application routes. This middleware validates the access token on every request to secured endpoints. For APIs, consider reading from `Authorization: Bearer` headers instead of cookies to minimize CSRF risk. Here’s an example middleware method validating the access token and refreshing it if expired for every request. * Node.js middleware/auth.js ```javascript 1 async function verifyToken(req, res, next) { 2 // Extract encrypted tokens from request cookies 3 const { accessToken, refreshToken } = req.cookies; 4 5 if (!accessToken) { 6 return res.status(401).json({ error: 'Authentication required' }); 7 } 8 9 try { 10 // Decrypt the access token before validation 11 const decryptedAccessToken = decrypt(accessToken); 12 13 // Verify token validity using Scalekit's validation method 14 const isValid = await scalekit.validateAccessToken(decryptedAccessToken); 15 16 if (!isValid && refreshToken) { 17 // Token expired - refresh it transparently 18 const decryptedRefreshToken = decrypt(refreshToken); 19 const authResult = await scalekit.refreshAccessToken(decryptedRefreshToken); 20 21 // Encrypt and store new tokens 22 res.cookie('accessToken', encrypt(authResult.accessToken), { 23 maxAge: (authResult.expiresIn - 60) * 1000, 24 httpOnly: true, 25 secure: process.env.NODE_ENV === 'production', 26 sameSite: 'strict' 27 }); 28 29 res.cookie('refreshToken', encrypt(authResult.refreshToken), { 30 httpOnly: true, 31 secure: process.env.NODE_ENV === 'production', 32 sameSite: 'strict' 33 }); 34 35 return next(); 36 } 37 38 if (!isValid) { 39 return res.status(401).json({ error: 'Session expired. Please sign in again.' }); 40 } 41 42 // Token is valid, proceed to the next middleware or route handler 43 next(); 44 } catch (error) { 45 return res.status(401).json({ error: 'Authentication failed' }); 46 } 47 } ``` * Python middleware/auth.py ```python 2 collapsed lines 1 from flask import request, jsonify 2 from functools import wraps 3 def verify_token(f): 4 @wraps(f) 5 def decorated_function(*args, **kwargs): 6 # Extract encrypted tokens from request cookies 7 access_token = request.cookies.get('accessToken') 8 refresh_token = request.cookies.get('refreshToken') 9 10 if not access_token: 11 return jsonify({'error': 'Authentication required'}), 401 12 13 try: 14 # Decrypt the access token before validation 15 decrypted_access_token = decrypt(access_token) 16 17 # Verify token validity using Scalekit's validation method 18 is_valid = scalekit_client.validate_access_token(decrypted_access_token) 19 20 if not is_valid and refresh_token: 21 # Token expired - refresh it transparently 22 decrypted_refresh_token = decrypt(refresh_token) 23 auth_result = scalekit_client.refresh_access_token(decrypted_refresh_token) 24 25 # Encrypt and store new tokens 26 response = make_response(f(*args, **kwargs)) 27 response.set_cookie( 28 'accessToken', 29 encrypt(auth_result.access_token), 30 max_age=auth_result.expires_in - 60, 31 httponly=True, 32 secure=os.environ.get('FLASK_ENV') == 'production', 33 samesite='Strict' 34 ) 35 response.set_cookie( 36 'refreshToken', 37 encrypt(auth_result.refresh_token), 38 httponly=True, 39 secure=os.environ.get('FLASK_ENV') == 'production', 40 samesite='Strict' 41 ) 42 return response 43 44 if not is_valid: 45 return jsonify({'error': 'Session expired. Please sign in again.'}), 401 46 47 # Token is valid, proceed to the protected view function 48 return f(*args, **kwargs) 49 50 except Exception: 51 return jsonify({'error': 'Authentication failed'}), 401 52 53 return decorated_function ``` * Go 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 } ``` * Java 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 } ``` 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 1 import type { JWTPayload } from 'jose'; 2 import type { IdTokenClaim } from '@scalekit-sdk/node'; 3 4 // Access token — typed as JWTPayload 5 const claims = await scalekit.validateToken(accessToken); 6 console.log(claims.sub); // user ID 7 8 // ID token — typed with full user profile claims 9 const idClaims = await scalekit.validateToken(idToken); 10 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. ![](/.netlify/images?url=_astro%2Fsession-policies-dashboard.BpRLl4UP.png\&w=3052\&h=1918\&dpl=69cce21a4f77360008b1503a) 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. 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.session.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.session.create_session_filter( 14 status=["ACTIVE"], start_time=start_time, end_time=end_time 15 ) 16 user_sessions = scalekit_client.session.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.session.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.session.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"); ``` Your application continuously validates the access token for each incoming request. When the token is valid, the user’s session remains active. If the access token expires, your middleware transparently refreshes it using the stored refresh token—users never notice this happening. If the refresh token itself expires or becomes invalid, users are prompted to sign in again. --- # 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ### 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=69cce21a4f77360008b1503a) * **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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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) ## 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 1 /oauth/authorize? 2 response_type=code& 3 client_id=& 4 redirect_uri=& 5 scope=openid+profile+email+offline_access& 6 state=& 7 code_challenge=& 8 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 1 POST /oauth/token 2 Content-Type: application/x-www-form-urlencoded 3 4 grant_type=authorization_code& 5 client_id=& 6 code=& 7 redirect_uri=& 8 code_verifier= ``` ```json 1 { 2 "access_token": "...", 3 "refresh_token": "...", 4 "id_token": "...", 5 "expires_in": 299 6 } ``` 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 1 POST /oauth/token 2 Content-Type: application/x-www-form-urlencoded 3 4 grant_type=refresh_token& 5 client_id=& 6 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 1 /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 1 /oidc/logout? 2 id_token_hint=& 3 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 1 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 --- # 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. ## 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 --- # 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) ## 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 1 /oauth/authorize? 2 response_type=code& 3 client_id=& 4 redirect_uri=& 5 scope=openid+profile+email+offline_access& 6 state=& 7 code_challenge=& 8 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 1 POST /oauth/token 2 Content-Type: application/x-www-form-urlencoded 3 4 grant_type=authorization_code& 5 client_id=& 6 code=& 7 redirect_uri=& 8 code_verifier= ``` ```json 1 { 2 "access_token": "...", 3 "refresh_token": "...", 4 "id_token": "...", 5 "expires_in": 299 6 } ``` 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 1 POST /oauth/token 2 Content-Type: application/x-www-form-urlencoded 3 4 grant_type=refresh_token& 5 client_id=& 6 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 1 /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 1 /oidc/logout? 2 id_token_hint=& 3 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) ## 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 1 /oauth/authorize? 2 response_type=code& 3 client_id=& 4 redirect_uri=& 5 scope=openid+profile+email+offline_access& 6 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 1 POST /oauth/token 2 Content-Type: application/x-www-form-urlencoded 3 4 grant_type=authorization_code& 5 client_id=& 6 client_secret=& 7 code=& 8 redirect_uri= ``` ```json 1 { 2 "access_token": "...", 3 "refresh_token": "...", 4 "id_token": "...", 5 "expires_in": 299 6 } ``` 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 1 POST /oauth/token 2 Content-Type: application/x-www-form-urlencoded 3 4 grant_type=refresh_token& 5 client_id=& 6 client_secret=& 7 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 1 /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 1 /oidc/logout? 2 id_token_hint=& 3 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 --- # Quickstart > 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) See the integration in action [Play](https://youtube.com/watch?v=Gnz8FYhHKI8) 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=69cce21a4f77360008b1503a) 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 ### Build with a coding agent * Claude Code ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` ```bash /plugin install full-stack-auth@scalekit-auth-stack ``` * Codex ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` ```bash # Restart Codex # Plugin Directory -> Scalekit Auth Stack -> install full-stack-auth ``` * GitHub Copilot CLI ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` ```bash copilot plugin install full-stack-auth@scalekit-auth-stack ``` * 40+ agents ```bash npx skills add scalekit-inc/skills --skill implementing-scalekit-fsa ``` [Continue building with AI →](/dev-kit/build-with-ai/full-stack-auth/) *** 1. ## Install Scalekit SDK [Section titled “Install Scalekit SDK”](#install-scalekit-sdk) 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` 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= ``` 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. Register redirect URLs in your Scalekit dashboard Before creating the authorization URL, register redirect URLs in your Scalekit dashboard. Go to **Scalekit dashboard** → **Authentication** → **Redirect URLs** and configure: * **Allowed callback URL**: The endpoint where Scalekit sends users after successful authentication. The `redirect_uri` in your code must match this URL exactly. [Learn more](/guides/dashboard/redirects/#allowed-callback-urls) * **Initiate-login URL**: 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) Now, you can **create an authorization URL** to redirect users to the login page. * Node.js routes/auth.ts ```javascript 1 // Must match the allowed callback URL you registered in the dashboard 2 const 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 const options = { 7 scopes: ['openid', 'profile', 'email', 'offline_access'] 8 }; 9 10 const authorizationUrl = scalekit.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%2Fauth%2Fcallback 13 14 res.redirect(authorizationUrl); ``` * Python app/auth/routes.py ```python 1 from scalekit import AuthorizationUrlOptions 2 3 # Must match the allowed callback URL you registered in the dashboard 4 redirect_uri = 'http://localhost:3000/auth/callback' 5 6 # Request user profile data (openid, profile, email) and session tracking (offline_access) 7 # offline_access enables refresh tokens so users can stay logged in across sessions 8 options = AuthorizationUrlOptions() 9 options.scopes = ['openid', 'profile', 'email', 'offline_access'] 10 11 12 authorization_url = scalekit.get_authorization_url(redirect_uri, options) 13 # Generated URL will look like: 14 # https:///oauth/authorize?response_type=code&client_id=skc_1234&scope=openid%20profile%20email%20offline_access&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback 15 16 return redirect(authorization_url) ``` * Go 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()) ``` * Java AuthController.java ```java 1 import com.scalekit.internal.http.AuthorizationUrlOptions; 2 import java.net.URL; 3 import java.util.Arrays; 4 5 // Must match the allowed callback URL you registered in the dashboard 6 String redirectUri = "http://localhost:3000/auth/callback"; 7 8 // Request user profile data (openid, profile, email) and session tracking (offline_access) 9 // offline_access enables refresh tokens so users can stay logged in across sessions 10 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 11 options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access")); 12 13 URL authorizationUrl = scalekit.authentication().getAuthorizationUrl(redirectUri, options); 14 // Generated URL will look like: 15 // 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 routes/auth-callback.ts ```javascript 1 import scalekit from '@/utils/auth.js' 2 const redirectUri = ''; 3 4 // Get the authorization code from the scalekit initiated callback 5 app.get('/auth/callback', async (req, res) => { 6 collapsed lines 6 const { code, error, error_description } = req.query; 7 8 if (error) { 9 return res.status(401).json({ error, error_description }); 10 } 11 12 try { 13 // Exchange the authorization code for user profile and session tokens 14 // Returns: user (profile info), idToken (JWT with user claims), accessToken (JWT with roles/permissions), refreshToken 15 const authResult = await scalekit.authenticateWithCode( 16 code, redirectUri 17 ); 18 8 collapsed lines 19 const { user, idToken, accessToken, refreshToken } = authResult; 20 // idToken: Decode to access full user profile (sub, oid, email, name) 21 // accessToken: Contains roles and permissions for authorization decisions 22 // refreshToken: Use to obtain new access tokens when they expire 23 24 // "user" object contains the user's profile information 25 // Next step: Create a session and log in the user 26 res.redirect('/dashboard/profile'); 27 } catch (err) { 28 console.error('Error exchanging code:', err); 29 res.status(500).json({ error: 'Failed to authenticate user' }); 30 } 31 }); ``` * Python app/auth/callback.py ```python 6 collapsed lines 1 from flask import Flask, request, redirect, jsonify 2 from scalekit import ScalekitClient, CodeAuthenticationOptions 3 4 app = Flask(__name__) 5 # scalekit imported from your auth utils 6 7 redirect_uri = 'http://localhost:3000/auth/callback' 8 9 @app.route('/auth/callback') 10 def callback(): 11 code = request.args.get('code') 12 error = request.args.get('error') 13 error_description = request.args.get('error_description') 14 15 if error: 16 return jsonify({'error': error, 'error_description': error_description}), 401 17 18 try: 19 # Exchange the authorization code for user profile and session tokens 20 # Returns: user (profile info), id_token (JWT with user claims), access_token (JWT with roles/permissions), refresh_token 21 options = CodeAuthenticationOptions() 22 auth_result = scalekit.authenticate_with_code( 23 code, redirect_uri, options 24 ) 25 26 user = auth_result["user"] 27 # id_token: Decode to access full user profile (sub, oid, email, name) 28 # access_token: Contains roles and permissions for authorization decisions 4 collapsed lines 29 # refresh_token: Use to obtain new access tokens when they expire 30 31 # "user" object contains the user's profile information 32 # Next step: Create a session and log in the user 33 return redirect('/dashboard/profile') 34 except Exception as err: 35 print(f'Error exchanging code: {err}') 36 return jsonify({'error': 'Failed to authenticate user'}), 500 ``` * Go 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 } ``` * Java 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 ```javascript 4 collapsed lines 1 import cookieParser from 'cookie-parser'; 2 // Set cookie parser middleware 3 app.use(cookieParser()); 4 5 // Store access token in HttpOnly cookie with Path scoping to API routes 6 res.cookie('accessToken', authResult.accessToken, { 7 maxAge: (authResult.expiresIn - 60) * 1000, 8 httpOnly: true, 9 secure: true, 10 path: '/api', 11 sameSite: 'strict' 12 }); 13 14 // Store refresh token in separate HttpOnly cookie with Path scoped to refresh endpoint 15 res.cookie('refreshToken', authResult.refreshToken, { 16 httpOnly: true, 17 secure: true, 18 path: '/auth/refresh', 19 sameSite: 'strict' 20 }); ``` * Python ```python 10 collapsed lines 1 from flask import Flask, make_response 2 import os 3 4 # Cookie parsing is built-in with Flask's request object 5 app = Flask(__name__) 6 7 response = make_response() 8 9 # Store access token in HttpOnly cookie with Path scoping to API routes 10 response.set_cookie( 11 'accessToken', 12 auth_result.access_token, 13 max_age=auth_result.expires_in - 60, # seconds in Flask 14 httponly=True, 15 secure=True, 16 path='/api', 17 samesite='Strict' 18 ) 19 20 # Store refresh token in separate HttpOnly cookie with Path scoped to refresh endpoint 21 response.set_cookie( 22 'refreshToken', 23 auth_result.refresh_token, 24 httponly=True, 25 secure=True, 26 path='/auth/refresh', 27 samesite='Strict' 28 ) ``` * Go ```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 ) ``` * Java ```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 ```javascript 1 // Middleware to verify and refresh tokens if needed 2 const verifyToken = async (req, res, next) => { 3 try { 4 // Get access token from cookie and decrypt it 5 const accessToken = req.cookies.accessToken; 6 const decryptedAccessToken = decrypt(accessToken); 7 4 collapsed lines 8 if (!accessToken) { 9 return res.status(401).json({ message: 'No access token provided' }); 10 } 11 12 // Use Scalekit SDK to validate the token 13 const isValid = await scalekit.validateAccessToken(decryptedAccessToken); 14 15 if (!isValid) { 16 // Use stored refreshToken to get a new access token 17 const { 18 user, 19 idToken, 20 accessToken, 21 refreshToken: newRefreshToken, 22 } = await scalekit.refreshAccessToken(refreshToken); 23 24 // Store the new refresh token 25 // Update the cookie with the new access token 12 collapsed lines 26 } 27 next(); 28 }; 29 30 // Example of using the middleware to protect routes 31 app.get('/dashboard', verifyToken, (req, res) => { 32 // The user object is now available in req.user 33 res.json({ 34 message: 'This is a protected route', 35 user: req.user 36 }); 37 }); ``` * Python ```python 3 collapsed lines 1 from functools import wraps 2 from flask import request, jsonify, make_response 3 4 def verify_token(f): 5 """Decorator to verify and refresh tokens if needed""" 6 @wraps(f) 7 def decorated_function(*args, **kwargs): 8 try: 9 # Get access token from cookie 10 access_token = request.cookies.get('accessToken') 4 collapsed lines 11 12 if not access_token: 13 return jsonify({'message': 'No access token provided'}), 401 14 15 # Decrypt the accessToken using the same encryption algorithm 16 decrypted_access_token = decrypt(access_token) 17 18 # Use Scalekit SDK to validate the token 19 is_valid = scalekit.validate_access_token(decrypted_access_token) 20 21 if not is_valid: 6 collapsed lines 22 # Get stored refresh token 23 refresh_token = get_stored_refresh_token() 24 25 if not refresh_token: 26 return jsonify({'message': 'No refresh token available'}), 401 27 28 # Use stored refreshToken to get a new access token 29 token_response = scalekit.refresh_access_token(refresh_token) 30 31 # Python SDK returns dict with access_token and refresh_token 32 new_access_token = token_response.get('access_token') 33 new_refresh_token = token_response.get('refresh_token') 34 35 # Store the new refresh token 36 store_refresh_token(new_refresh_token) 37 38 # Update the cookie with the new access token 39 encrypted_new_access_token = encrypt(new_access_token) 40 response = make_response(f(*args, **kwargs)) 41 response.set_cookie( 42 'accessToken', 43 encrypted_new_access_token, 44 httponly=True, 45 secure=True, 46 path='/', 47 samesite='Strict' 48 ) 49 50 return response 17 collapsed lines 51 52 # If the token was valid we just invoke the view as-is 53 return f(*args, **kwargs) 54 55 except Exception as e: 56 return jsonify({'message': f'Token verification failed: {str(e)}'}), 401 57 58 return decorated_function 59 60 # Example of using the decorator to protect routes 61 @app.route('/dashboard') 62 @verify_token 63 def dashboard(): 64 return jsonify({ 65 'message': 'This is a protected route', 66 'user': getattr(request, 'user', None) 67 }) ``` * Go ```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)) ``` * Java ```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 ```javascript 1 app.get('/logout', (req, res) => { 2 // Clear all session data including cookies and local storage 3 clearSessionData(); 4 5 const logoutUrl = scalekit.getLogoutUrl( 6 idTokenHint, // ID token to invalidate 7 postLogoutRedirectUri // URL that scalekit redirects after session invalidation 8 ); 9 10 // Redirect the user to the Scalekit logout endpoint to begin invalidating the session. 11 res.redirect(logoutUrl); // This URL can only be used once and expires after logout. 12 }); ``` * Python ```python 5 collapsed lines 1 from flask import Flask, redirect 2 from scalekit.common.scalekit import LogoutUrlOptions 3 4 app = Flask(__name__) 5 6 @app.route('/logout') 7 def logout(): 8 # Clear all session data including cookies and local storage 9 clear_session_data() 10 11 # Generate Scalekit logout URL 12 options = LogoutUrlOptions( 13 id_token_hint=id_token, 14 post_logout_redirect_uri=post_logout_redirect_uri 15 ) 16 logout_url = scalekit.get_logout_url(options) 17 18 # Redirect to Scalekit's logout endpoint 19 # Note: This is a one-time use URL that becomes invalid after use 20 return redirect(logout_url) ``` * Go ```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 } ``` * Java ```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 --- # 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. ![](/.netlify/images?url=_astro%2F2-configuration.BBcHzaot.png\&w=2786\&h=1746\&dpl=69cce21a4f77360008b1503a) ### 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: ![](/.netlify/images?url=_astro%2Flimit-org-users.F8VX5klf.png\&w=2454\&h=618\&dpl=69cce21a4f77360008b1503a) 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. ![](/.netlify/images?url=_astro%2F1-user-profile.CQCsGgPh.png\&w=2786\&h=1746\&dpl=69cce21a4f77360008b1503a) 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. 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 that triggered the event (e.g., "DirectoryUser", "Directory", "Connection"). "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 ID of the organization associated with the event. "type": "organization.directory.user_created", // The specific event type (e.g., "organization.directory.user_created"). "data": { // Event-specific payload containing details relevant to the event type. "user_id": "usr_123456789", "email": "user@example.com", "name": "John Doe" } } ``` ## 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 **Settings** > **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 from the dropdown: * **User events** - `user.created`, `user.updated`, `user.deleted` * **Organization events** - `organization.created`, `organization.updated` * **Authentication events** - `session.created`, `session.expired` * **Membership events** - `membership.created`, `membership.updated`, `membership.deleted` 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 should respond with a `201` status code within 10 seconds to be considered successful. Failed deliveries are retried up to 3 times with exponential backoff. ## 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(/* your credentials */); 6 7 // Use raw body parser for webhook signature verification 8 app.use('/webhooks/scalekit', express.raw({ type: 'application/json' })); 9 10 app.post('/webhooks/scalekit', async (req, res) => { 11 try { 12 // Get webhook signature from headers 13 const signature = req.headers['scalekit-signature']; 14 const rawBody = req.body; 15 16 // Verify webhook signature using Scalekit SDK 17 const isValid = await scalekit.webhooks.verifySignature( 18 rawBody, 19 signature, 20 process.env.SCALEKIT_WEBHOOK_SECRET 21 ); 22 23 if (!isValid) { 24 console.error('Invalid webhook signature'); 25 return res.status(401).json({ error: 'Invalid signature' }); 26 } 27 28 // Parse and process the webhook payload 29 const event = JSON.parse(rawBody.toString()); 30 await processWebhookEvent(event); 31 32 // Always respond with 200 to acknowledge receipt 33 res.status(200).json({ received: true }); 34 35 } catch (error) { 36 console.error('Webhook processing error:', error); 37 res.status(500).json({ error: 'Webhook processing failed' }); 38 } 39 }); ``` * Python Flask webhook handler ```python 4 collapsed lines 1 from flask import Flask, request, jsonify 2 import json 3 from scalekit import ScalekitClient 4 5 app = Flask(__name__) 6 scalekit_client = ScalekitClient(/* your credentials */) 7 8 @app.route('/webhooks/scalekit', methods=['POST']) 9 def handle_webhook(): 10 try: 11 # Get webhook signature from headers 12 signature = request.headers.get('scalekit-signature') 13 raw_body = request.get_data() 14 15 # Verify webhook signature using Scalekit SDK 16 is_valid = scalekit_client.webhooks.verify_signature( 17 raw_body, 18 signature, 19 os.environ.get('SCALEKIT_WEBHOOK_SECRET') 20 ) 21 22 if not is_valid: 23 print('Invalid webhook signature') 24 return jsonify({'error': 'Invalid signature'}), 401 25 26 # Parse and process the webhook payload 27 event = json.loads(raw_body.decode('utf-8')) 28 process_webhook_event(event) 29 30 # Always respond with 200 to acknowledge receipt 31 return jsonify({'received': True}), 200 32 33 except Exception as error: 34 print(f'Webhook processing error: {error}') 35 return jsonify({'error': 'Webhook processing failed'}), 500 ``` * Go Gin webhook handler ```go 8 collapsed lines 1 package main 2 3 import ( 4 "encoding/json" 5 "io" 6 "net/http" 7 "github.com/gin-gonic/gin" 8 "github.com/scalekit-inc/scalekit-sdk-go" 9 ) 10 11 scalekitClient := scalekit.NewScalekitClient(/* your credentials */) 12 13 func handleWebhook(c *gin.Context) { 14 // Get webhook signature from headers 15 signature := c.GetHeader("scalekit-signature") 16 17 // Read raw body 18 rawBody, err := io.ReadAll(c.Request.Body) 19 if err != nil { 20 c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read body"}) 21 return 22 } 23 24 // Verify webhook signature using Scalekit SDK 25 isValid, err := scalekitClient.Webhooks.VerifySignature( 26 rawBody, 27 signature, 28 os.Getenv("SCALEKIT_WEBHOOK_SECRET"), 29 ) 30 31 if err != nil || !isValid { 32 c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"}) 33 return 34 } 35 36 // Parse and process the webhook payload 37 var event map[string]interface{} 38 if err := json.Unmarshal(rawBody, &event); err != nil { 39 c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"}) 40 return 41 } 42 43 processWebhookEvent(event) 44 45 // Always respond with 200 to acknowledge receipt 46 c.JSON(http.StatusOK, gin.H{"received": true}) 47 } ``` * Java Spring webhook handler ```java 8 collapsed lines 1 import org.springframework.web.bind.annotation.*; 2 import org.springframework.http.ResponseEntity; 3 import org.springframework.http.HttpStatus; 4 import com.scalekit.ScalekitClient; 5 import com.fasterxml.jackson.databind.ObjectMapper; 6 import javax.servlet.http.HttpServletRequest; 7 import java.io.IOException; 8 9 @RestController 10 public class WebhookController { 11 12 private final ScalekitClient scalekitClient; 13 private final ObjectMapper objectMapper = new ObjectMapper(); 14 15 @PostMapping("/webhooks/scalekit") 16 public ResponseEntity> handleWebhook( 17 HttpServletRequest request, 18 @RequestBody String rawBody 19 ) { 20 try { 21 // Get webhook signature from headers 22 String signature = request.getHeader("scalekit-signature"); 23 24 // Verify webhook signature using Scalekit SDK 25 boolean isValid = scalekitClient.webhooks().verifySignature( 26 rawBody.getBytes(), 27 signature, 28 System.getenv("SCALEKIT_WEBHOOK_SECRET") 29 ); 30 31 if (!isValid) { 32 return ResponseEntity.status(HttpStatus.UNAUTHORIZED) 33 .body(Map.of("error", "Invalid signature")); 34 } 35 36 // Parse and process the webhook payload 37 Map event = objectMapper.readValue(rawBody, Map.class); 38 processWebhookEvent(event); 39 40 // Always respond with 200 to acknowledge receipt 41 return ResponseEntity.ok(Map.of("received", true)); 42 43 } catch (Exception error) { 44 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) 45 .body(Map.of("error", "Webhook processing failed")); 46 } 47 } 48 } ``` 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.created': 6 // Handle new user registration 7 await handleUserCreated(event.data.user, event.data.organization); 8 break; 9 10 case 'user.updated': 11 // Handle user profile updates 12 await handleUserUpdated(event.data.user); 13 break; 14 15 case 'organization.created': 16 // Handle new organization creation 17 await handleOrganizationCreated(event.data.organization); 18 break; 19 20 case 'membership.created': 21 // Handle user joining organization 22 await handleMembershipCreated(event.data.membership); 23 break; 24 25 default: 26 console.log(`Unhandled event type: ${event.type}`); 27 } 28 } 29 30 async function handleUserCreated(user, organization) { 31 // Use case: Sync new user to your database, send welcome email, set up user workspace 32 console.log(`New user created: ${user.email} in org: ${organization.display_name}`); 33 34 // Sync to your database 35 await syncUserToDatabase(user, organization); 36 37 // Send welcome email 38 await sendWelcomeEmail(user.email, user.first_name); 39 40 // Set up user workspace or default settings 41 await setupUserDefaults(user.id, organization.id); 42 } ``` * 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.created': 8 # Handle new user registration 9 handle_user_created(event_data['user'], event_data['organization']) 10 elif event_type == 'user.updated': 11 # Handle user profile updates 12 handle_user_updated(event_data['user']) 13 elif event_type == 'organization.created': 14 # Handle new organization creation 15 handle_organization_created(event_data['organization']) 16 elif event_type == 'membership.created': 17 # Handle user joining organization 18 handle_membership_created(event_data['membership']) 19 else: 20 print(f'Unhandled event type: {event_type}') 21 22 def handle_user_created(user, organization): 23 # Use case: Sync new user to your database, send welcome email, set up user workspace 24 print(f'New user created: {user["email"]} in org: {organization["display_name"]}') 25 26 # Sync to your database 27 sync_user_to_database(user, organization) 28 29 # Send welcome email 30 send_welcome_email(user['email'], user['first_name']) 31 32 # Set up user workspace or default settings 33 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.created": 9 // Handle new user registration 10 user := eventData["user"].(map[string]interface{}) 11 organization := eventData["organization"].(map[string]interface{}) 12 handleUserCreated(user, organization) 13 14 case "user.updated": 15 // Handle user profile updates 16 user := eventData["user"].(map[string]interface{}) 17 handleUserUpdated(user) 18 19 case "organization.created": 20 // Handle new organization creation 21 organization := eventData["organization"].(map[string]interface{}) 22 handleOrganizationCreated(organization) 23 24 case "membership.created": 25 // Handle user joining organization 26 membership := eventData["membership"].(map[string]interface{}) 27 handleMembershipCreated(membership) 28 29 default: 30 fmt.Printf("Unhandled event type: %s\n", eventType) 31 } 32 } 33 34 func handleUserCreated(user, organization map[string]interface{}) { 35 // Use case: Sync new user to your database, send welcome email, set up user workspace 36 fmt.Printf("New user created: %s in org: %s\n", 37 user["email"], organization["display_name"]) 38 39 // Sync to your database 40 syncUserToDatabase(user, organization) 41 42 // Send welcome email 43 sendWelcomeEmail(user["email"].(string), user["first_name"].(string)) 44 45 // Set up user workspace or default settings 46 setupUserDefaults(user["id"].(string), organization["id"].(string)) 47 } ``` * 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.created": 9 // Handle new user registration 10 Map user = (Map) eventData.get("user"); 11 Map organization = (Map) eventData.get("organization"); 12 handleUserCreated(user, organization); 13 break; 14 15 case "user.updated": 16 // Handle user profile updates 17 handleUserUpdated((Map) eventData.get("user")); 18 break; 19 20 case "organization.created": 21 // Handle new organization creation 22 handleOrganizationCreated((Map) eventData.get("organization")); 23 break; 24 25 case "membership.created": 26 // Handle user joining organization 27 handleMembershipCreated((Map) eventData.get("membership")); 28 break; 29 30 default: 31 System.out.println("Unhandled event type: " + eventType); 32 } 33 } 34 35 private void handleUserCreated(Map user, Map organization) { 36 // Use case: Sync new user to your database, send welcome email, set up user workspace 37 System.out.println("New user created: " + user.get("email") + 38 " in org: " + organization.get("display_name")); 39 40 // Sync to your database 41 syncUserToDatabase(user, organization); 42 43 // Send welcome email 44 sendWelcomeEmail((String) user.get("email"), (String) user.get("first_name")); 45 46 // Set up user workspace or default settings 47 setupUserDefaults((String) user.get("id"), (String) organization.get("id")); 48 } ``` 3. ### Verify webhook signature [Section titled “Verify webhook signature”](#verify-webhook-signature) Use the Scalekit SDK to verify webhook signatures before processing events. * Node.js Signature verification ```javascript 1 async function verifyWebhookSignature(rawBody, signature, secret) { 2 try { 3 // Use Scalekit SDK for verification (recommended) 4 const isValid = await scalekit.webhooks.verifySignature(rawBody, signature, secret); 5 return isValid; 6 7 } catch (error) { 8 console.error('Signature verification failed:', error); 9 return false; 10 } 11 } ``` * Python Signature verification ```python 1 def verify_webhook_signature(raw_body, signature, secret): 2 try: 3 # Use Scalekit SDK for verification (recommended) 4 is_valid = scalekit_client.webhooks.verify_signature(raw_body, signature, secret) 5 return is_valid 6 7 except Exception as error: 8 print(f'Signature verification failed: {error}') 9 return False ``` * Go Signature verification ```go 1 func verifyWebhookSignature(rawBody []byte, signature string, secret string) (bool, error) { 2 // Use Scalekit SDK for verification (recommended) 3 isValid, err := scalekitClient.Webhooks.VerifySignature(rawBody, signature, secret) 4 if err != nil { 5 fmt.Printf("Signature verification failed: %v\n", err) 6 return false, err 7 } 8 return isValid, nil 9 } ``` * Java Signature verification ```java 1 private boolean verifyWebhookSignature(byte[] rawBody, String signature, String secret) { 2 try { 3 // Use Scalekit SDK for verification (recommended) 4 boolean isValid = scalekitClient.webhooks().verifySignature(rawBody, signature, secret); 5 return isValid; 6 7 } catch (Exception error) { 8 System.err.println("Signature verification failed: " + error.getMessage()); 9 return false; 10 } 11 } ``` Caution Security: Always verify webhook signatures before processing events. This prevents unauthorized parties from triggering your webhook handlers with malicious payloads. ## 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 failed webhooks automatically using exponential backoff. Return appropriate error codes to avoid unnecessary retries. * **Initial retry**: Immediately after failure * **Subsequent retries**: 5 seconds, 30 seconds, 2 minutes, 5 minutes, 15 minutes * **Maximum attempts**: 6 total attempts over approximately 22 minutes * **Final failure**: Webhook marked as failed after all retries exhausted 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 1 # Install ngrok 2 npm install -g ngrok 3 4 # Start your local server 5 npm run dev 6 7 # In another terminal, expose your local server 8 ngrok http 3000 9 10 # Use the ngrok URL in your Scalekit dashboard 11 # 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 > Apply decision checks at key points in the authentication flow Execute custom business logic during sign-up or login processes. For example, you can integrate with external systems to validate user existence before allowing login, or prevent sign-ups originating from suspicious IP addresses. Scalekit calls your application at key trigger points during authentication flows and waits for an ALLOW or DENY response to determine whether to continue with the authentication process. For example, one trigger point occurs immediately before a user signs up for your application. We’ll explore more trigger points throughout this guide. ## Implementing interceptors [Section titled “Implementing interceptors”](#implementing-interceptors) You can define interceptors at several trigger points during authentication flows. | Trigger point | When it runs | | ---------------------- | --------------------------------------------------------------------- | | Pre-signup | Before a user creates a new organization | | Pre-session creation | Before session tokens are issued for a user | | Pre-user invitation | Before an invitation is created or sent for a new organization member | | Pre-M2M token creation | Before issuing a machine-to-machine access token | At each trigger point, Scalekit sends a POST request to your interceptor endpoint with the relevant details needed to process the request. 1. #### Verify the interceptor request [Section titled “Verify the interceptor request”](#verify-the-interceptor-request) Create an HTTPS endpoint that receives and verifies POST requests from Scalekit. This critical security step ensures requests are authentic and haven’t been tampered with. * Node.js Express.js - Verify request signature ```javascript 1 // Security: ALWAYS verify requests are from Scalekit before processing 2 // This prevents unauthorized parties from triggering your interceptor logic 3 4 app.post('/auth/interceptors/pre-signup', async (req, res) => { 5 try { 6 // Parse the request payload and headers 7 const event = await req.json(); 8 const headers = req.headers; 9 10 // Get the signing secret from Scalekit dashboard > Interceptors tab 11 // Store this securely in environment variables 12 const interceptorSecret = process.env.SCALEKIT_INTERCEPTOR_SECRET; 13 14 // Initialize Scalekit client (reference installation guide for setup) 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 // Verify the interceptor payload signature 22 // This confirms the request is from Scalekit and hasn't been tampered with 23 await scalekit.verifyInterceptorPayload(interceptorSecret, headers, event); 24 25 // ✓ Request verified - proceed to business logic (next step) 26 27 } catch (error) { 28 console.error('Interceptor verification failed:', error); 29 // DENY on verification failures to fail securely 30 return res.status(200).json({ 31 decision: 'DENY', 32 error: { 33 message: 'Unable to process request. Please try again later.' 34 } 35 }); 36 } 37 }); ``` * Python Flask - Verify request signature ```python 1 # Security: ALWAYS verify requests are from Scalekit before processing 2 # This prevents unauthorized parties from triggering your interceptor logic 3 4 from flask import Flask, request, jsonify 5 import os 6 7 app = Flask(__name__) 8 9 @app.route('/auth/interceptors/pre-signup', methods=['POST']) 10 def interceptor_pre_signup(): 11 try: 12 # Parse the request payload and headers 13 event = request.get_json() 14 body = request.get_data() 15 16 # Get the signing secret from Scalekit dashboard > Interceptors tab 17 # Store this securely in environment variables 18 interceptor_secret = os.getenv('SCALEKIT_INTERCEPTOR_SECRET') 19 20 # Extract headers for verification 21 headers = { 22 'interceptor-id': request.headers.get('interceptor-id'), 23 'interceptor-signature': request.headers.get('interceptor-signature'), 24 'interceptor-timestamp': request.headers.get('interceptor-timestamp') 25 } 26 27 # Initialize Scalekit client (reference installation guide for setup) 28 scalekit_client = ScalekitClient( 29 env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), 30 client_id=os.getenv("SCALEKIT_CLIENT_ID"), 31 client_secret=os.getenv("SCALEKIT_CLIENT_SECRET") 32 ) 33 34 # Verify the interceptor payload signature 35 # This confirms the request is from Scalekit and hasn't been tampered with 36 is_valid = scalekit_client.verify_interceptor_payload( 37 secret=interceptor_secret, 38 headers=headers, 39 payload=body 40 ) 41 42 if not is_valid: 43 return jsonify({ 44 'decision': 'DENY', 45 'error': {'message': 'Invalid request signature'} 46 }), 200 47 48 # ✓ Request verified - proceed to business logic (next step) 49 50 except Exception as error: 51 print(f'Interceptor verification failed: {error}') 52 # DENY on verification failures to fail securely 53 return jsonify({ 54 'decision': 'DENY', 55 'error': { 56 'message': 'Unable to process request. Please try again later.' 57 } 58 }), 200 ``` * Go Gin - Verify request signature ```go 1 // Security: ALWAYS verify requests are from Scalekit before processing 2 // This prevents unauthorized parties from triggering your interceptor logic 3 4 package main 5 6 import ( 7 "io" 8 "log" 9 "net/http" 10 "os" 11 12 "github.com/gin-gonic/gin" 13 ) 14 15 type InterceptorResponse struct { 16 Decision string `json:"decision"` 17 Error *InterceptorError `json:"error,omitempty"` 18 } 19 20 type InterceptorError struct { 21 Message string `json:"message"` 22 } 23 24 func interceptorPreSignup(c *gin.Context) { 25 // Parse the request payload 26 bodyBytes, err := io.ReadAll(c.Request.Body) 27 if err != nil { 28 c.JSON(http.StatusOK, InterceptorResponse{ 29 Decision: "DENY", 30 Error: &InterceptorError{Message: "Unable to read request"}, 31 }) 32 return 33 } 34 35 // Get the signing secret from Scalekit dashboard > Interceptors tab 36 // Store this securely in environment variables 37 interceptorSecret := os.Getenv("SCALEKIT_INTERCEPTOR_SECRET") 38 39 // Extract headers for verification 40 headers := map[string]string{ 41 "interceptor-id": c.GetHeader("interceptor-id"), 42 "interceptor-signature": c.GetHeader("interceptor-signature"), 43 "interceptor-timestamp": c.GetHeader("interceptor-timestamp"), 44 } 45 46 // Initialize Scalekit client (reference installation guide for setup) 47 scalekitClient := scalekit.NewScalekitClient( 48 os.Getenv("SCALEKIT_ENVIRONMENT_URL"), 49 os.Getenv("SCALEKIT_CLIENT_ID"), 50 os.Getenv("SCALEKIT_CLIENT_SECRET"), 51 ) 52 53 // Verify the interceptor payload signature 54 // This confirms the request is from Scalekit and hasn't been tampered with 55 _, err = scalekitClient.VerifyInterceptorPayload( 56 interceptorSecret, 57 headers, 58 bodyBytes, 59 ) 60 if err != nil { 61 log.Printf("Interceptor verification failed: %v", err) 62 // DENY on verification failures to fail securely 63 c.JSON(http.StatusOK, InterceptorResponse{ 64 Decision: "DENY", 65 Error: &InterceptorError{Message: "Invalid request signature"}, 66 }) 67 return 68 } 69 70 // ✓ Request verified - proceed to business logic (next step) 71 } ``` * Java Spring Boot - Verify request signature ```java 1 // Security: ALWAYS verify requests are from Scalekit before processing 2 // This prevents unauthorized parties from triggering your interceptor logic 3 4 package com.example.auth; 5 6 import org.springframework.http.ResponseEntity; 7 import org.springframework.web.bind.annotation.*; 8 9 import java.util.Map; 10 11 @RestController 12 @RequestMapping("/auth/interceptors") 13 public class InterceptorController { 14 15 @PostMapping("/pre-signup") 16 public ResponseEntity> preSignupInterceptor( 17 @RequestBody String body, 18 @RequestHeader Map headers 19 ) { 20 try { 21 // Get the signing secret from Scalekit dashboard > Interceptors tab 22 // Store this securely in environment variables 23 String interceptorSecret = System.getenv("SCALEKIT_INTERCEPTOR_SECRET"); 24 25 // Initialize Scalekit client (reference installation guide for setup) 26 ScalekitClient scalekitClient = new ScalekitClient( 27 System.getenv("SCALEKIT_ENVIRONMENT_URL"), 28 System.getenv("SCALEKIT_CLIENT_ID"), 29 System.getenv("SCALEKIT_CLIENT_SECRET") 30 ); 31 32 // Verify the interceptor payload signature 33 // This confirms the request is from Scalekit and hasn't been tampered with 34 boolean valid = scalekitClient.interceptor() 35 .verifyInterceptorPayload(interceptorSecret, headers, body.getBytes()); 36 37 if (!valid) { 38 // DENY on invalid signatures 39 return ResponseEntity.ok(Map.of( 40 "decision", "DENY", 41 "error", Map.of("message", "Invalid request signature") 42 )); 43 } 44 45 // ✓ Request verified - proceed to business logic (next step) 46 47 } catch (Exception error) { 48 System.err.println("Interceptor verification failed: " + error.getMessage()); 49 // DENY on verification failures to fail securely 50 return ResponseEntity.ok(Map.of( 51 "decision", "DENY", 52 "error", Map.of( 53 "message", "Unable to process request. Please try again later." 54 ) 55 )); 56 } 57 } 58 } ``` 2. #### Implement business logic and respond [Section titled “Implement business logic and respond”](#implement-business-logic-and-respond) After verification, extract data from the payload, apply your custom validation logic, and return either ALLOW or DENY to control the authentication flow. * Node.js Express.js - Business logic and response ```javascript 1 // Use case: Apply custom validation rules before allowing authentication 2 // Examples: email domain validation, IP filtering, database checks, etc. 3 4 app.post('/auth/interceptors/pre-signup', async (req, res) => { 5 try { 6 // ... (verification code from Step 1) 7 8 // Extract data from the verified payload 9 const { interceptor_context, data } = event; 10 const userEmail = interceptor_context?.user_email || data?.user?.email; 11 12 // Implement your business logic 13 // Example: Validate email domain against an allowlist 14 const emailDomain = userEmail?.split('@')[1]; 15 const allowedDomains = ['company.com', 'example.com']; 16 17 if (!allowedDomains.includes(emailDomain)) { 18 // DENY: Block the authentication flow 19 return res.status(200).json({ 20 decision: 'DENY', 21 error: { 22 message: 'Sign-ups from this email domain are not permitted.' 23 } 24 }); 25 } 26 27 // Optional: Log successful validations for audit purposes 28 console.log(`Allowed signup for ${userEmail}`); 29 30 // ALLOW: Permit the authentication flow to continue 31 return res.status(200).json({ 32 decision: 'ALLOW' 33 }); 34 35 } catch (error) { 36 console.error('Interceptor error:', error); 37 return res.status(200).json({ 38 decision: 'DENY', 39 error: { 40 message: 'Unable to process request. Please try again later.' 41 } 42 }); 43 } 44 }); ``` * Python Flask - Business logic and response ```python 1 # Use case: Apply custom validation rules before allowing authentication 2 # Examples: email domain validation, IP filtering, database checks, etc. 3 4 @app.route('/auth/interceptors/pre-signup', methods=['POST']) 5 def interceptor_pre_signup(): 6 try: 7 # ... (verification code from Step 1) 8 9 # Extract data from the verified payload 10 interceptor_context = event.get('interceptor_context', {}) 11 data = event.get('data', {}) 12 user_email = interceptor_context.get('user_email') or data.get('user', {}).get('email') 13 14 # Implement your business logic 15 # Example: Validate email domain against an allowlist 16 email_domain = user_email.split('@')[1] if user_email else '' 17 allowed_domains = ['company.com', 'example.com'] 18 19 if email_domain not in allowed_domains: 20 # DENY: Block the authentication flow 21 return jsonify({ 22 'decision': 'DENY', 23 'error': { 24 'message': 'Sign-ups from this email domain are not permitted.' 25 } 26 }), 200 27 28 # Optional: Log successful validations for audit purposes 29 print(f'Allowed signup for {user_email}') 30 31 # ALLOW: Permit the authentication flow to continue 32 return jsonify({ 33 'decision': 'ALLOW' 34 }), 200 35 36 except Exception as error: 37 print(f'Interceptor error: {error}') 38 return jsonify({ 39 'decision': 'DENY', 40 'error': { 41 'message': 'Unable to process request. Please try again later.' 42 } 43 }), 200 ``` * Go Gin - Business logic and response ```go 1 // Use case: Apply custom validation rules before allowing authentication 2 // Examples: email domain validation, IP filtering, database checks, etc. 3 4 package main 5 6 import ( 7 "encoding/json" 8 "strings" 9 ) 10 11 type InterceptorEvent struct { 12 InterceptorContext struct { 13 UserEmail string `json:"user_email"` 14 } `json:"interceptor_context"` 15 Data struct { 16 User struct { 17 Email string `json:"email"` 18 } `json:"user"` 19 } `json:"data"` 20 } 21 22 func interceptorPreSignup(c *gin.Context) { 23 // ... (verification code from Step 1) 24 25 // Extract data from the verified payload 26 var event InterceptorEvent 27 if err := json.Unmarshal(bodyBytes, &event); err != nil { 28 c.JSON(http.StatusOK, InterceptorResponse{ 29 Decision: "DENY", 30 Error: &InterceptorError{Message: "Invalid request format"}, 31 }) 32 return 33 } 34 35 userEmail := event.InterceptorContext.UserEmail 36 if userEmail == "" { 37 userEmail = event.Data.User.Email 38 } 39 40 // Implement your business logic 41 // Example: Validate email domain against an allowlist 42 parts := strings.Split(userEmail, "@") 43 if len(parts) != 2 { 44 c.JSON(http.StatusOK, InterceptorResponse{ 45 Decision: "DENY", 46 Error: &InterceptorError{Message: "Invalid email address"}, 47 }) 48 return 49 } 50 51 emailDomain := parts[1] 52 allowedDomains := []string{"company.com", "example.com"} 53 54 allowed := false 55 for _, domain := range allowedDomains { 56 if emailDomain == domain { 57 allowed = true 58 break 59 } 60 } 61 62 if !allowed { 63 // DENY: Block the authentication flow 64 c.JSON(http.StatusOK, InterceptorResponse{ 65 Decision: "DENY", 66 Error: &InterceptorError{ 67 Message: "Sign-ups from this email domain are not permitted.", 68 }, 69 }) 70 return 71 } 72 73 // Optional: Log successful validations for audit purposes 74 log.Printf("Allowed signup for %s", userEmail) 75 76 // ALLOW: Permit the authentication flow to continue 77 c.JSON(http.StatusOK, InterceptorResponse{ 78 Decision: "ALLOW", 79 }) 80 } ``` * Java Spring Boot - Business logic and response ```java 1 // Use case: Apply custom validation rules before allowing authentication 2 // Examples: email domain validation, IP filtering, database checks, etc. 3 4 package com.example.auth; 5 6 import com.fasterxml.jackson.databind.JsonNode; 7 import com.fasterxml.jackson.databind.ObjectMapper; 8 9 import java.util.Arrays; 10 import java.util.List; 11 12 @PostMapping("/pre-signup") 13 public ResponseEntity> preSignupInterceptor( 14 @RequestBody String body, 15 @RequestHeader Map headers 16 ) { 17 try { 18 // ... (verification code from Step 1) 19 20 // Extract data from the verified payload 21 ObjectMapper mapper = new ObjectMapper(); 22 JsonNode event = mapper.readTree(body); 23 JsonNode interceptorContext = event.get("interceptor_context"); 24 JsonNode data = event.get("data"); 25 26 String userEmail = null; 27 if (interceptorContext != null && interceptorContext.has("user_email")) { 28 userEmail = interceptorContext.get("user_email").asText(); 29 } else if (data != null && data.has("user")) { 30 userEmail = data.get("user").get("email").asText(); 31 } 32 33 // Implement your business logic 34 // Example: Validate email domain against an allowlist 35 if (userEmail != null && userEmail.contains("@")) { 36 String emailDomain = userEmail.split("@")[1]; 37 List allowedDomains = Arrays.asList("company.com", "example.com"); 38 39 if (!allowedDomains.contains(emailDomain)) { 40 // DENY: Block the authentication flow 41 return ResponseEntity.ok(Map.of( 42 "decision", "DENY", 43 "error", Map.of( 44 "message", "Sign-ups from this email domain are not permitted." 45 ) 46 )); 47 } 48 } 49 50 // Optional: Log successful validations for audit purposes 51 System.out.println("Allowed signup for " + userEmail); 52 53 // ALLOW: Permit the authentication flow to continue 54 return ResponseEntity.ok(Map.of( 55 "decision", "ALLOW" 56 )); 57 58 } catch (Exception error) { 59 System.err.println("Interceptor error: " + error.getMessage()); 60 return ResponseEntity.ok(Map.of( 61 "decision", "DENY", 62 "error", Map.of( 63 "message", "Unable to process request. Please try again later." 64 ) 65 )); 66 } 67 } ``` 3. #### Register the interceptor in Scalekit dashboard [Section titled “Register the interceptor in Scalekit dashboard”](#register-the-interceptor-in-scalekit-dashboard) Configure your interceptor by specifying the trigger point, endpoint URL, timeout settings, and fallback behavior. In the Scalekit dashboard, navigate to the **Interceptors** tab to register your endpoint. ![Interceptors settings in the Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-interceptor-page.XX7OLoCR.png\&w=2084\&h=1588\&dpl=69cce21a4f77360008b1503a) * Enter a descriptive name, choose a trigger point, and provide the HTTPS endpoint that will receive POST requests * Set the timeout for your app’s response (recommended: 3-5 seconds) * Choose the fallback behavior if your app fails or times out (allow or block the flow) * Click **Create** * Toggle **Enable** to activate the interceptor 4. #### Test the interceptor [Section titled “Test the interceptor”](#test-the-interceptor) Use the Test tab in the Scalekit dashboard to verify your implementation before enabling it in production. * Open the **Test** tab on the Interceptors page * The left panel shows the request body sent to your endpoint * Click **Send request** to test your interceptor * The right panel shows your application’s response * Verify your endpoint returns the expected ALLOW or DENY decision ![Interceptor test tab example](/.netlify/images?url=_astro%2Ftest-example.xdLJLh_5.png\&w=2970\&h=1643\&dpl=69cce21a4f77360008b1503a) Quick testing with request bin services For quick testing without building or deploying an endpoint, use a request bin service like [Beeceptor](https://beeceptor.com/) or [RequestBin](https://requestbin.com/). These services provide temporary endpoints that capture incoming requests and let you configure responses, making them ideal for interceptor development and validation. 5. #### View interceptor request logs [Section titled “View interceptor request logs”](#view-interceptor-request-logs) Scalekit keeps a log of every interceptor request sent to your app and the response it returned. Use these logs to debug and troubleshoot issues. ![Interceptor logs in the dashboard](/.netlify/images?url=_astro%2Flogging.DSZdvTsn.png\&w=3024\&h=1705\&dpl=69cce21a4f77360008b1503a) Requests and responses generated by the “Test” button are not logged. This keeps production logs free of test data. Generic error messages Scalekit shows a generic error to end users when: * Your interceptor returns `DENY` without an `error.message`. * The interceptor request fails or times out and the fail policy is set to “Fail closed”. Messages shown: * “The request could not be completed due to a policy restriction. Please contact support for assistance.” * “The request could not be completed due to a policy restriction. Please contact for assistance.” (when a support email is configured) ## Interceptor examples [Section titled “Interceptor examples”](#interceptor-examples) ### Block signups from restricted IP addresses [Section titled “Block signups from restricted IP addresses”](#block-signups-from-restricted-ip-addresses) Prevent new user signups from specific IP addresses or geographic regions. The request includes `ip_address` and `region` (country code) in `interceptor_context`. * Node.js Express.js ```javascript 1 app.post('/auth/interceptor/pre-signup', async (req, res) => { 2 const { interceptor_context } = req.body; 3 4 // Extract IP address and region from the request 5 const ipAddress = interceptor_context.ip_address; 6 const region = interceptor_context.region; 7 8 // Define your IP blocklist (you can also check against a database) 9 const blockedIPs = ['203.0.113.24', '198.51.100.42']; 10 const blockedRegions = ['XX', 'YY']; // Example: blocked region codes 11 12 // Check if IP is blocked 13 if (blockedIPs.includes(ipAddress)) { 14 return res.json({ 15 decision: 'DENY', 16 error: { 17 message: 'Signups from your IP address are not allowed due to security policy' 18 } 19 }); 20 } 21 22 // Check if region is blocked 23 if (blockedRegions.includes(region)) { 24 return res.json({ 25 decision: 'DENY', 26 error: { 27 message: 'Signups from your location are restricted due to compliance requirements' 28 } 29 }); 30 } 31 32 // Allow signup to proceed 33 return res.json({ 34 decision: 'ALLOW' 35 }); 36 }); ``` * Python Flask ```python 1 @app.post('/auth/interceptor/pre-signup') 2 collapsed lines 2 async def pre_signup(request: Request): 3 body = await request.json() 4 interceptor_context = body['interceptor_context'] 5 6 # Extract IP address and region from the request 7 ip_address = interceptor_context['ip_address'] 8 region = interceptor_context['region'] 9 10 # Define your IP blocklist (you can also check against a database) 11 blocked_ips = ['203.0.113.24', '198.51.100.42'] 12 blocked_regions = ['XX', 'YY'] # Example: blocked region codes 13 14 # Check if IP is blocked 15 if ip_address in blocked_ips: 16 return { 17 'decision': 'DENY', 18 'error': { 19 'message': 'Signups from your IP address are not allowed due to security policy' 20 } 21 } 22 23 # Check if region is blocked 24 if region in blocked_regions: 25 return { 26 'decision': 'DENY', 27 'error': { 28 'message': 'Signups from your location are restricted due to compliance requirements' 29 } 30 } 31 32 # Allow signup to proceed 33 return {'decision': 'ALLOW'} ``` ### Modify claims in session tokens [Section titled “Modify claims in session tokens”](#modify-claims-in-session-tokens) Add custom claims to Access tokens issued by Scalekit. Fetch user metadata from your database and return claims in the `response.claims` object. Claims are automatically included in the access token after authentication. * Node.js Express.js ```javascript 1 app.post('/auth/interceptor/pre-session-creation', async (req, res) => { 2 const { interceptor_context } = req.body; 3 4 const userId = interceptor_context.user_id; 5 const organizationId = interceptor_context.organization_id; 6 7 // Fetch user subscription and permissions from your database 8 const userMetadata = await fetchUserMetadata(userId, organizationId); 9 10 // Build custom claims based on your business logic 11 const customClaims = { 12 plan: userMetadata.subscription.plan, // 'free', 'pro', 'enterprise' 13 plan_expires_at: userMetadata.subscription.expiresAt, 14 features: userMetadata.features, // ['analytics', 'api_access', 'advanced_reports'] 15 org_role: userMetadata.organizationRole, // 'admin', 'member', 'viewer' 16 department: userMetadata.department, 17 cost_center: userMetadata.costCenter 18 }; 19 20 // Return ALLOW decision with custom claims 21 return res.json({ 22 decision: 'ALLOW', 23 response: { 24 claims: customClaims 25 } 26 }); 27 }); ``` * Python Flask ```python 1 @app.post('/auth/interceptor/pre-session-creation') 2 async def pre_session_creation(request: Request): 3 body = await request.json() 4 interceptor_context = body['interceptor_context'] 5 6 user_id = interceptor_context['user_id'] 7 organization_id = interceptor_context['organization_id'] 8 9 # Fetch user subscription and permissions from your database 10 user_metadata = await fetch_user_metadata(user_id, organization_id) 11 12 # Build custom claims based on your business logic 13 custom_claims = { 14 'plan': user_metadata['subscription']['plan'], 15 'plan_expires_at': user_metadata['subscription']['expires_at'], 16 'features': user_metadata['features'], 17 'org_role': user_metadata['organization_role'], 18 'department': user_metadata['department'], 19 'cost_center': user_metadata['cost_center'] 20 } 21 22 # Return ALLOW decision with custom claims 23 return { 24 'decision': 'ALLOW', 25 'response': { 26 'claims': custom_claims 27 } 28 } ``` After the interceptor returns custom claims, Scalekit includes them in the access token. When you decode the access token, it contains your custom claims in the `custom_claims` object along with standard JWT fields: Decoded access token ```diff { "aud": [ "prd_skc_96736847635480854" ], "client_id": "prd_skc_96736847635480854", "custom_claims": { "cost_center": "R&D-001", "department": "Engineering", "features": [ "analytics", "api_access", "advanced_reports" + ], "org_role": "admin", "plan": "pro", "plan_expires_at": "2025-12-31T23:59:59Z" + }, "exp": 1767964824, "iat": 1767964524, "iss": "https://auth.coffeedesk.app", "jti": "tkn_107201921814692618", "nbf": 1767964524, "oid": "org_97926637244383515", 12 collapsed lines "permissions": [ "data:read", "data:write", "organization:settings" ], "roles": [ "admin" ], "sid": "ses_107201917586768386", "sub": "usr_97931091561677319", "xoid": "wspace_97926637244383515", "xuid": "0a749c69-1153-4a8b-b56d-94ebde9da8de" } ``` Token size considerations Keep custom claims minimal to avoid exceeding JWT size limits. Store large datasets in your database and use claims only for frequently-accessed metadata that needs to be available in the token. ### Provision a user into an existing organization [Section titled “Provision a user into an existing organization”](#provision-a-user-into-an-existing-organization) Use the **Pre-signup** interceptor to provision a user into an existing organization instead of creating a new one during signup. This is useful when you want users from specific email domains to always join a pre-defined organization, avoiding duplicate organization creation. In the following example, the B2B application provisions users into an existing organization based on their email domain. If no matching domain is found, the signup flow falls back to the default behavior and creates a new organization. * Node.js Express.js ```javascript 1 app.post('/auth/interceptors/pre-signup', async (req, res) => { 2 const { interceptor_context } = req.body; 3 4 // Email attempting to sign up 5 const userEmail = interceptor_context.user_email; 6 const emailDomain = userEmail?.split('@')[1]; 7 8 // Map email domains to organizations 9 const domainOrgMappings = [ 10 { 11 domain: 'acmecorp.com', 12 organization_id: 'org_123456789', 13 external_organization_id: 'ext_acmecorp_123' 14 }, 15 { 16 domain: 'megacorp.com', 17 organization_id: 'org_987654321', 18 external_organization_id: 'ext_megacorp_456' 19 } 20 ]; 21 22 const match = domainOrgMappings.find( 23 (entry) => entry.domain === emailDomain 24 ); 25 26 // Fallback to default signup behavior 27 if (!match) { 28 return res.json({ decision: 'ALLOW' }); 29 } 30 31 return res.json({ 32 decision: 'ALLOW', 33 response: { 34 create_organization_membership: { 35 // Either external_organization_id or organization_id is required 36 organization_id: match.organization_id, 37 external_organization_id: match.external_organization_id 38 } 39 } 40 }); 41 }); ``` * Python ```python 1 @app.post('/auth/interceptors/pre-signup') 2 def pre_signup(): 3 body = request.get_json() 4 5 interceptor_context = body.get('interceptor_context', {}) 6 7 # Email attempting to sign up 8 user_email = interceptor_context.get('user_email') 9 email_domain = user_email.split('@')[1] if user_email else None 10 11 # Map email domains to organizations 12 domain_org_mappings = [ 13 { 14 domain: 'acmecorp.com', 15 organization_id: 'org_123456789', 16 external_organization_id: 'ext_acmecorp_123' 17 }, 18 { 19 domain: 'megacorp.com', 20 organization_id: 'org_987654321', 21 external_organization_id: 'ext_megacorp_456' 22 } 23 ] 24 25 match = next( 26 (entry for entry in domain_org_mappings if entry['domain'] == email_domain), 27 None 28 ) 29 30 # Fallback to default signup behavior 31 if not match: 32 return {'decision': 'ALLOW'} 33 34 return { 35 'decision': 'ALLOW', 36 'response': { 37 'create_organization_membership': { 38 # Either external_organization_id or organization_id is required 39 'organization_id': match.get('organization_id'), 40 'external_organization_id': match.get('external_organization_id') 41 } 42 } 43 } ``` --- # 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. Here’s how it works: 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 1 curl -X POST \ 2 "https:///oauth/token" \ 3 -H "Content-Type: application/x-www-form-urlencoded" \ 4 -d "grant_type=client_credentials" \ 5 -d "client_id=" \ 6 -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 1 { 2 "access_token":"", 3 "token_type":"Bearer", 4 "expires_in":86399, 5 // Same scopes that were granted during client registration 6 "scope":"deploy:applications read:deployments" 7 } ``` 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) endpoint. 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: 'YOUR_JWKS_URI', 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) Scopes 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 1 curl -L 'https:///api/v1/organizations//clients' \ 2 -H 'Content-Type: application/json' \ 3 -H 'Authorization: Bearer ' \ 4 -d '{ 5 "name": "GitHub Actions Deployment Service", 6 "description": "Service account for GitHub Actions to deploy applications to production", 7 "scopes": [ 8 "deploy:applications", 9 "read:deployments" 10 ], 11 "expiry": 3600 12 }' ``` 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}/.well-known/jwks.json`, 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. 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` Initialize the Scalekit client with your environment credentials: * Node.js Express.js ```javascript 2 collapsed lines 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 ); ``` * Python Flask ```python 2 collapsed lines 1 import os 2 from scalekit import ScalekitClient 3 4 scalekit_client = ScalekitClient( 5 env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], 6 client_id=os.environ["SCALEKIT_CLIENT_ID"], 7 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 8 ) ``` * Go Gin ```go 2 collapsed lines 1 import scalekit "github.com/scalekit-inc/scalekit-sdk-go/v2" 2 3 scalekitClient := scalekit.NewScalekitClient( 4 os.Getenv("SCALEKIT_ENVIRONMENT_URL"), 5 os.Getenv("SCALEKIT_CLIENT_ID"), 6 os.Getenv("SCALEKIT_CLIENT_SECRET"), 7 ) ``` * Java Spring Boot ```java 2 collapsed lines 1 import com.scalekit.ScalekitClient; 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 ); ``` 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 1 try { 2 const response = await scalekit.token.createToken(organizationId, { 3 description: 'CI/CD pipeline token', 4 }); 5 6 // Store securely — this value cannot be retrieved again after creation 7 const opaqueToken = response.token; 8 // Stable identifier for management operations (format: apit_xxxxx) 9 const tokenId = response.tokenId; 10 } catch (error) { 11 console.error('Failed to create token:', error.message); 12 } ``` * Python ```python 1 try: 2 response = scalekit_client.tokens.create_token( 3 organization_id=organization_id, 4 description="CI/CD pipeline token", 5 ) 6 7 opaque_token = response.token # store this securely 8 token_id = response.token_id # format: apit_xxxxx 9 except Exception as e: 10 print(f"Failed to create token: {e}") ``` * Go ```go 1 response, err := scalekitClient.Token().CreateToken( 2 ctx, organizationId, scalekit.CreateTokenOptions{ 3 Description: "CI/CD pipeline token", 4 }, 5 ) 6 if err != nil { 7 log.Printf("Failed to create token: %v", err) 8 return 9 } 10 11 // Store securely — this value cannot be retrieved again after creation 12 opaqueToken := response.Token 13 // Stable identifier for management operations (format: apit_xxxxx) 14 tokenId := response.TokenId ``` * Java ```java 1 import com.scalekit.grpc.scalekit.v1.tokens.CreateTokenResponse; 2 3 try { 4 CreateTokenResponse response = scalekitClient.tokens().create(organizationId); 5 6 // Store securely — this value cannot be retrieved again after creation 7 String opaqueToken = response.getToken(); 8 // Stable identifier for management operations (format: apit_xxxxx) 9 String tokenId = response.getTokenId(); 10 } catch (Exception e) { 11 System.err.println("Failed to create token: " + e.getMessage()); 12 } ``` ### 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 1 try { 2 const userToken = await scalekit.token.createToken(organizationId, { 3 userId: 'usr_12345', 4 customClaims: { 5 team: 'engineering', 6 environment: 'production', 7 }, 8 description: 'Deployment service token', 9 }); 10 11 const opaqueToken = userToken.token; 12 const tokenId = userToken.tokenId; 13 } catch (error) { 14 console.error('Failed to create token:', error.message); 15 } ``` * Python ```python 1 try: 2 user_token = scalekit_client.tokens.create_token( 3 organization_id=organization_id, 4 user_id="usr_12345", 5 custom_claims={ 6 "team": "engineering", 7 "environment": "production", 8 }, 9 description="Deployment service token", 10 ) 11 12 opaque_token = user_token.token 13 token_id = user_token.token_id 14 except Exception as e: 15 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 1 import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node'; 2 3 try { 4 const result = await scalekit.token.validateToken(opaqueToken); 5 6 const orgId = result.tokenInfo?.organizationId; 7 const userId = result.tokenInfo?.userId; 8 const claims = result.tokenInfo?.customClaims; 9 } catch (error) { 10 if (error instanceof ScalekitValidateTokenFailureException) { 11 // Token is invalid, expired, or revoked 12 console.error('Token validation failed:', error.message); 13 } 14 } ``` * Python ```python 1 from scalekit import ScalekitValidateTokenFailureException 2 3 try: 4 result = scalekit_client.tokens.validate_token(token=opaque_token) 5 6 org_id = result.token_info.organization_id 7 user_id = result.token_info.user_id 8 claims = result.token_info.custom_claims 9 except ScalekitValidateTokenFailureException: 10 # Token is invalid, expired, or revoked 11 print("Token validation failed") ``` * Go ```go 1 result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) 2 if errors.Is(err, scalekit.ErrTokenValidationFailed) { 3 // Token is invalid, expired, or revoked 4 log.Printf("Token validation failed: %v", err) 5 return 6 } 7 8 orgId := result.TokenInfo.OrganizationId 9 userId := result.TokenInfo.GetUserId() // *string — nil for org-scoped tokens 10 claims := result.TokenInfo.CustomClaims ``` * 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 orgId = result.getTokenInfo().getOrganizationId(); 9 String userId = result.getTokenInfo().getUserId(); 10 Map claims = result.getTokenInfo().getCustomClaimsMap(); 11 } catch (TokenInvalidException e) { 12 // Token is invalid, expired, or revoked 13 System.err.println("Token validation failed: " + e.getMessage()); 14 } ``` 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 1 try { 2 const result = await scalekit.token.validateToken(opaqueToken); 3 4 // Roles assigned to the user 5 const roles = result.tokenInfo?.roles; 6 7 // External identifiers for mapping to your system 8 const externalOrgId = result.tokenInfo?.organizationExternalId; 9 const externalUserId = result.tokenInfo?.userExternalId; 10 } catch (error) { 11 if (error instanceof ScalekitValidateTokenFailureException) { 12 console.error('Token validation failed:', error.message); 13 } 14 } ``` * Python ```python 1 try: 2 result = scalekit_client.tokens.validate_token(token=opaque_token) 3 4 # Roles assigned to the user 5 roles = result.token_info.roles 6 7 # External identifiers for mapping to your system 8 external_org_id = result.token_info.organization_external_id 9 external_user_id = result.token_info.user_external_id 10 except ScalekitValidateTokenFailureException: 11 print("Token validation failed") ``` * Go ```go 1 result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) 2 if errors.Is(err, scalekit.ErrTokenValidationFailed) { 3 log.Printf("Token validation failed: %v", err) 4 return 5 } 6 7 // Roles assigned to the user 8 roles := result.TokenInfo.Roles 9 10 // External identifiers for mapping to your system 11 externalOrgId := result.TokenInfo.OrganizationExternalId 12 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 1 try { 2 const result = await scalekit.token.validateToken(opaqueToken); 3 4 const team = result.tokenInfo?.customClaims?.team; 5 const environment = result.tokenInfo?.customClaims?.environment; 6 7 // Use metadata for authorization 8 if (environment !== 'production') { 9 return res.status(403).json({ error: 'Production access required' }); 10 } 11 } catch (error) { 12 if (error instanceof ScalekitValidateTokenFailureException) { 13 console.error('Token validation failed:', error.message); 14 } 15 } ``` * Python ```python 1 try: 2 result = scalekit_client.tokens.validate_token(token=opaque_token) 3 4 team = result.token_info.custom_claims.get("team") 5 environment = result.token_info.custom_claims.get("environment") 6 7 # Use metadata for authorization 8 if environment != "production": 9 return jsonify({"error": "Production access required"}), 403 10 except ScalekitValidateTokenFailureException: 11 print("Token validation failed") ``` * Go ```go 1 result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken) 2 if errors.Is(err, scalekit.ErrTokenValidationFailed) { 3 log.Printf("Token validation failed: %v", err) 4 return 5 } 6 7 team := result.TokenInfo.CustomClaims["team"] 8 environment := result.TokenInfo.CustomClaims["environment"] 9 10 // Use metadata for authorization 11 if environment != "production" { 12 c.JSON(403, gin.H{"error": "Production access required"}) 13 return 14 } ``` * 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 1 try { 2 // Invalidate by API key string 3 await scalekit.token.invalidateToken(opaqueToken); 4 5 // Or invalidate by token_id (useful when you store tokenId for lifecycle management) 6 await scalekit.token.invalidateToken(tokenId); 7 } catch (error) { 8 console.error('Failed to invalidate token:', error.message); 9 } ``` * Python ```python 1 try: 2 # Invalidate by API key string 3 scalekit_client.tokens.invalidate_token(token=opaque_token) 4 5 # Or invalidate by token_id (useful when you store token_id for lifecycle management) 6 scalekit_client.tokens.invalidate_token(token=token_id) 7 except Exception as e: 8 print(f"Failed to invalidate token: {e}") ``` * Go ```go 1 // Invalidate by API key string 2 if err := scalekitClient.Token().InvalidateToken(ctx, opaqueToken); err != nil { 3 log.Printf("Failed to invalidate token: %v", err) 4 } 5 6 // Or invalidate by token_id (useful when you store tokenId for lifecycle management) 7 if err := scalekitClient.Token().InvalidateToken(ctx, tokenId); err != nil { 8 log.Printf("Failed to invalidate token: %v", err) 9 } ``` * Java ```java 1 try { 2 // Invalidate by API key string 3 scalekitClient.tokens().invalidate(opaqueToken); 4 5 // Or invalidate by token_id (useful when you store tokenId for lifecycle management) 6 scalekitClient.tokens().invalidate(tokenId); 7 } catch (Exception e) { 8 System.err.println("Failed to invalidate token: " + e.getMessage()); 9 } ``` 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 1 // Handle indirect auth entry points 2 app.get('/login', (req, res) => { 3 const redirectUri = 'http://localhost:3000/auth/callback'; 4 const options = { 5 scopes: ['openid', 'profile', 'email', 'offline_access'] 6 }; 7 8 const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options); 9 res.redirect(authorizationUrl); 10 }); ``` * Python routes/auth.py ```python 1 from flask import redirect 2 from scalekit import AuthorizationUrlOptions 3 4 # Handle indirect auth entry points 5 @app.route('/login') 6 def login(): 7 redirect_uri = 'http://localhost:3000/auth/callback' 8 options = AuthorizationUrlOptions( 9 scopes=['openid', 'profile', 'email', 'offline_access'] 10 ) 11 12 authorization_url = scalekit_client.get_authorization_url(redirect_uri, options) 13 return redirect(authorization_url) ``` * Go routes/auth.go ```go 1 // Handle indirect auth entry points 2 r.GET("/login", func(c *gin.Context) { 3 redirectUri := "http://localhost:3000/auth/callback" 4 options := scalekitClient.AuthorizationUrlOptions{ 5 Scopes: []string{"openid", "profile", "email", "offline_access"} 6 } 7 8 authorizationUrl, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options) 9 c.Redirect(http.StatusFound, authorizationUrl.String()) 10 }) ``` * Java AuthController.java ```java 1 import org.springframework.web.bind.annotation.GetMapping; 2 import org.springframework.web.bind.annotation.RestController; 3 import java.net.URL; 4 5 // Handle indirect auth entry points 6 @GetMapping("/login") 7 public String login() { 8 String redirectUri = "http://localhost:3000/auth/callback"; 9 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 10 options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access")); 11 12 URL authorizationUrl = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options); 13 return "redirect:" + authorizationUrl.toString(); 14 } ``` 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. 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. --- # 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 1 // Use case: Remove user during offboarding workflow triggered by HR system 2 await scalekit.user.deleteMembership({ 3 organizationId: 'org_12345', 4 userId: 'usr_67890' 5 }) ``` - Python Remove users from organizations ```python 1 # Use case: Remove user during offboarding workflow triggered by HR system 2 scalekit_client.users.delete_membership( 3 organization_id="org_12345", 4 user_id="usr_67890" 5 ) ``` - Go Remove users from organizations ```go 1 // Use case: Remove user during offboarding workflow triggered by HR system 2 err := scalekitClient.User().DeleteMembership(ctx, "org_123", "user_456", false) 3 if err != nil { 4 log.Printf("Failed to remove user: %v", err) 5 return err 6 } ``` - Java Remove users from organizations ```java 1 // Use case: Remove user during offboarding workflow triggered by HR system 2 try { 3 scalekitClient.user().deleteMembership("org_123", "user_456"); 4 } catch (Exception e) { 5 log.error("Failed to remove user: " + e.getMessage()); 6 throw e; 7 } ``` 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 1 const { organization } = await scalekit.organization.createOrganization( 2 'Orion Analytics' 3 ); 4 5 // Use case: Sync organization profile to downstream systems 6 const { organization: fetched } = await scalekit.organization.getOrganization(organization.id); ``` * Python Create and manage organizations ```python 1 from scalekit.v1.organizations.organizations_pb2 import CreateOrganization 2 3 response = scalekit_client.organization.create_organization( 4 CreateOrganization( 5 display_name="Orion Analytics", 6 ) 7 ) 8 9 # Use case: Sync organization profile to downstream systems 10 fetched = scalekit_client.organization.get_organization(response[0].organization.id) ``` * Go Create and manage organizations ```go 1 created, err := scalekitClient.Organization().CreateOrganization( 2 ctx, 3 "Orion Analytics", 4 scalekit.CreateOrganizationOptions{}, 5 ) 6 if err != nil { 7 log.Fatalf("create organization: %v", err) 8 } 9 10 // Use case: Sync organization profile to downstream systems 11 fetched, err := scalekitClient.Organization().GetOrganization(ctx, created.Organization.Id) 12 if err != nil { 13 log.Fatalf("get organization: %v", err) 14 } ``` * Java Create and manage organizations ```java 1 // Use case: Provision a workspace after a sales-assisted onboarding 2 CreateOrganization createOrganization = CreateOrganization.newBuilder() 3 .setDisplayName("Orion Analytics") 4 .build(); 5 6 Organization organization = scalekitClient.organizations().create(createOrganization); 7 8 // Use case: Sync organization profile to downstream systems 9 Organization fetched = scalekitClient.organizations().getById(organization.getId()); ``` 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 1 { 2 "id": "usp_96194455173923084", // Unique user identifier 3 "first_name": "John", // User's given name 4 "last_name": "Doe", // User's family name 5 "name": "John Doe", // Full name for UI display 6 "locale": "en-US", // User's language and region preference 7 "email_verified": true, // Whether the email address has been confirmed 8 "phone_number": "+14155552671", // Contact phone number 2 collapsed lines 9 "metadata": { }, // Additional, non-structured user data 10 "custom_attributes": {} // Business-specific user data 11 } ``` 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 1 { 2 "id": "usp_96194455173923084", // Unique user identifier 3 "first_name": "John", // User's given name 4 "last_name": "Doe", // User's family name 5 "name": "John Doe", // Full name for UI display 6 "locale": "en-US", // User's language and region preference 7 "email_verified": true, // Whether the email address has been confirmed 8 "phone_number": "+14155552671", // Contact phone number 9 "metadata": { }, // Additional, non-structured user data 10 "custom_attributes": { 11 "pin_number": "123456" 12 } 13 } ``` 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. 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 1 curl -L -X PATCH '/api/v1/users/' \ 2 -H 'Content-Type: application/json' \ 3 -H 'Authorization: Bearer ...2QA' \ 4 -d '{ 5 "user_profile": { 6 "custom_attributes": { 7 "zip_code": "90210" 8 } 9 } 10 }' ``` * Node.js Update user profile with custom attributes ```javascript 1 // Use case: Update user profile with a custom zip code attribute 2 await scalekit.user.updateUser("", { 3 userProfile: { 4 customAttributes: { 5 zip_code: "11120", 6 }, 7 firstName: "John", 8 lastName: "Doe", 9 locale: "en-US", 10 name: "John Michael Doe", 11 phoneNumber: "+14155552671" 12 } 13 }); ``` * Python Update user profile with custom attributes ```python 1 # Use case: Update user profile with a custom zip code attribute 2 scalekit.user.update_user( 3 "", 4 user_profile={ 5 "custom_attributes": { 6 "zip_code": "11120" 7 }, 8 "first_name": "John", 9 "last_name": "Doe", 10 "locale": "en-US", 11 "name": "John Michael Doe", 12 "phone_number": "+14155552671" 13 } 14 ) ``` * Go Update user profile with custom attributes ```go 1 // Use case: Update user profile with a custom zip code attribute 2 updateUser := &usersv1.UpdateUser{ 3 UserProfile: &usersv1.UpdateUserProfile{ 4 CustomAttributes: map[string]string{ 5 "zip_code": "11120", 6 }, 7 FirstName: "John", 8 LastName: "Doe", 9 Locale: "en-US", 10 Name: "John Michael Doe", 11 PhoneNumber: "+14155552671", 12 }, 13 } 14 15 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 1 // During user signup or organization creation 2 const organization = await scalekit.organization.create({ 3 display_name: 'Acme Corporation', 4 external_id: 'CUST-12345-ACME' // Your customer ID in your database 5 }); 6 7 console.log('Organization created:', organization.id); 8 console.log('Your ID:', organization.external_id); ``` * Python Create organization with external ID ```python 1 # During user signup or organization creation 2 organization = scalekit.organization.create({ 3 'display_name': 'Acme Corporation', 4 'external_id': 'CUST-12345-ACME' # Your customer ID in your database 5 }) 6 7 print(f'Organization created: {organization.id}') 8 print(f'Your ID: {organization.external_id}') ``` * Go Create organization with external ID ```go 1 // During user signup or organization creation 2 org, err := scalekit.Organization.Create(OrganizationCreateOptions{ 3 DisplayName: "Acme Corporation", 4 ExternalId: "CUST-12345-ACME", // Your customer ID in your database 5 }) 6 7 if err != nil { 8 log.Fatal(err) 9 } 10 11 fmt.Printf("Organization created: %s\n", org.Id) 12 fmt.Printf("Your ID: %s\n", org.ExternalId) ``` * Java Create organization with external ID ```java 1 // During user signup or organization creation 2 Organization organization = scalekit.organization().create( 3 "Acme Corporation", 4 "CUST-12345-ACME" // Your customer ID in your database 5 ); 6 7 System.out.println("Organization created: " + organization.getId()); 8 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 1 // When processing a webhook or customer update 2 const customerId = 'CUST-12345-ACME'; // From your webhook payload 3 4 const organization = await scalekit.organization.getByExternalId(customerId); 5 6 if (organization) { 7 console.log('Found organization:', organization.display_name); 8 // Process organization updates, sync data, etc. 9 } ``` * Python Find organization by external ID ```python 1 # When processing a webhook or customer update 2 customer_id = 'CUST-12345-ACME' # From your webhook payload 3 4 organization = scalekit.organization.get_by_external_id(customer_id) 5 6 if organization: 7 print(f'Found organization: {organization.display_name}') 8 # Process organization updates, sync data, etc. ``` * Go Find organization by external ID ```go 1 // When processing a webhook or customer update 2 customerId := "CUST-12345-ACME" // From your webhook payload 3 4 org, err := scalekit.Organization.GetByExternalId(customerId) 5 if err != nil { 6 log.Printf("Error finding organization: %v", err) 7 return 8 } 9 10 if org != nil { 11 fmt.Printf("Found organization: %s\n", org.DisplayName) 12 // Process organization updates, sync data, etc. 13 } ``` * Java Find organization by external ID ```java 1 // When processing a webhook or customer update 2 String customerId = "CUST-12345-ACME"; // From your webhook payload 3 4 Organization organization = scalekit.organization().getByExternalId(customerId); 5 6 if (organization != null) { 7 System.out.println("Found organization: " + organization.getDisplayName()); 8 // Process organization updates, sync data, etc. 9 } ``` 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 1 const updatedOrg = await scalekit.organization.update(organizationId, { 2 external_id: 'NEW-CUST-12345-ACME' 3 }); 4 5 console.log('External ID updated:', updatedOrg.external_id); ``` * Python Update external ID ```python 1 updated_org = scalekit.organization.update(organization_id, { 2 'external_id': 'NEW-CUST-12345-ACME' 3 }) 4 5 print(f'External ID updated: {updated_org.external_id}') ``` * Go Update external ID ```go 1 updatedOrg, err := scalekit.Organization.Update(organizationId, OrganizationUpdateOptions{ 2 ExternalId: "NEW-CUST-12345-ACME", 3 }) 4 5 fmt.Printf("External ID updated: %s\n", updatedOrg.ExternalId) ``` * Java Update external ID ```java 1 Organization updatedOrg = scalekit.organization().update(organizationId, Map.of( 2 "external_id", "NEW-CUST-12345-ACME" 3 )); 4 5 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 1 // Use case: Link user with external systems (CRM, HR) and track custom attributes in a single call 2 const updatedUser = await scalekit.user.updateUser("", { 3 externalId: "SALESFORCE-003921", 4 metadata: { 5 department: "Sales", 6 employeeId: "EMP-002", 7 territory: "West Coast", 8 quota: 150000, 9 crmAccountId: "ACC-789", 10 hubspotContactId: "12345", 11 + }, 12 }); ``` * Python Update user external ID and metadata ```diff 1 # Use case: Link user with external systems (CRM, HR) and track custom attributes in a single call 2 updated_user = scalekit.user.update_user( 3 "", 4 +external_id="SALESFORCE-003921", 5 +metadata={ 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 ) ``` * 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 1 // Use case: Look up Scalekit user when you have your system's user ID 2 const user = await scalekit.user.getUserByExternalId("", "SALESFORCE-003921"); 3 console.log(`Found user: ${user.email} with ID: ${user.id}`); ``` * Python Find user by external ID ```python 1 # Use case: Look up Scalekit user when you have your system's user ID 2 user = scalekit.user.get_user_by_external_id("", "SALESFORCE-003921") 3 print(f"Found user: {user['email']} with ID: {user['id']}") ``` * Go Find user by external ID ```go 1 // Use case: Look up Scalekit user when you have your system's user ID 2 user, err := scalekitClient.User().GetUserByExternalId( 3 ctx, 4 "", 5 "SALESFORCE-003921", 6 ) 7 if err != nil { 8 log.Printf("User not found: %v", err) 9 } else { 10 fmt.Printf("Found user: %s with ID: %s\n", user.Email, user.Id) 11 } ``` * Java Find user by external ID ```java 1 // Use case: Look up Scalekit user when you have your system's user ID 2 try { 3 GetUserByExternalIdResponse response = scalekitClient.users() 4 .getUserByExternalId("", "SALESFORCE-003921"); 5 6 User user = response.getUser(); 7 System.out.printf("Found user: %s with ID: %s%n", user.getEmail(), user.getId()); 8 } catch (Exception e) { 9 System.err.println("User not found: " + e.getMessage()); 10 } ``` 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 1 // Use case: User account closure, GDPR deletion requests, or cleaning up test accounts 2 await scalekit.user.deleteUser("usr_123"); ``` * Python Delete a user permanently ```python 1 # Use case: User account closure, GDPR deletion requests, or cleaning up test accounts 2 scalekit_client.users.delete_user( 3 user_id="usr_123" 4 ) ``` * Go Delete a user permanently ```go 1 // Use case: User account closure, GDPR deletion requests, or cleaning up test accounts 2 if err := scalekitClient.User().DeleteUser(ctx, "usr_123"); err != nil { 3 panic(err) 4 } ``` * Java Delete a user permanently ```java 1 // Use case: User account closure, GDPR deletion requests, or cleaning up test accounts 2 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 1 // Use case: Company closure, account restructuring, or removing test organizations 2 await scalekit.organization.deleteOrganization(organizationId); ``` * Python Delete an organization permanently ```python 1 # Use case: Company closure, account restructuring, or removing test organizations 2 scalekit_client.organization.delete_organization(organization_id) ``` * Go Delete an organization permanently ```go 1 // Use case: Company closure, account restructuring, or removing test organizations 2 err := scalekitClient.Organization().DeleteOrganization( 3 ctx, 4 organizationId 5 ) 6 if err != nil { 7 panic(err) 8 } ``` * Java Delete an organization permanently ```java 1 // Use case: Company closure, account restructuring, or removing test organizations 2 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: ### 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 ![](/.netlify/images?url=_astro%2Fui.D6G2x64L.png\&w=2858\&h=1611\&dpl=69cce21a4f77360008b1503a) 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. 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. ![](/.netlify/images?url=_astro%2Fdashboard.Cf5i9h8I.png\&w=2938\&h=1588\&dpl=69cce21a4f77360008b1503a) 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 user management widgets > Customers manage organizations and users for their workspace through hosted widgets Your customers, especially workspace administrators, want to manage organizations and users for their members. Scalekit provides a hosted widgets portal that lets your customers view and manage organizations, users, and settings for their workspace on their own—without you building custom UI. To integrate hosted widgets, redirect your organization members to the Hosted Widgets URL: Hosted widgets URL ```sh /ui/ # https://your-app-env.scalekit.com/ui/ ``` Scalekit verifies the organization member’s access permissions and automatically controls what they can access in the widgets. The widgets inherit your application’s [branding](/fsa/guides/login-page-branding/) and support your [custom domain](/guides/custom-domain/). ## 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 feature entitlements. A widget appears only if the user has the required permissions and the organization has the corresponding feature enabled. 1. ### 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. ![](/.netlify/images?url=_astro%2Forg_settings.XshZN6sS.png\&w=2936\&h=1592\&dpl=69cce21a4f77360008b1503a) 2. ### 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. ![](/.netlify/images?url=_astro%2Forg_member.pe4fgTMu.png\&w=2936\&h=1592\&dpl=69cce21a4f77360008b1503a) 3. ### 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. Note SSO widget visibility depends on the organization’s feature entitlements. It 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). ![](/.netlify/images?url=_astro%2Forg_sso.IHoRc3E6.png\&w=2936\&h=1592\&dpl=69cce21a4f77360008b1503a) 4. ### 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. Note SCIM widget visibility depends on the organization’s feature entitlements. It 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). ![](/.netlify/images?url=_astro%2Forg_scim.CBDzga3B.png\&w=2936\&h=1592\&dpl=69cce21a4f77360008b1503a) ## 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-level feature entitlements or Scalekit permissions. 1. ### 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. ![](/.netlify/images?url=_astro%2Fuser_profile.DF85cQEC.png\&w=2936\&h=1592\&dpl=69cce21a4f77360008b1503a) 2. ### 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. ![](/.netlify/images?url=_astro%2Fuser_security.B5SWg3po.png\&w=2936\&h=1592\&dpl=69cce21a4f77360008b1503a) ## Access management [Section titled “Access management”](#access-management) Hosted 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 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 | Note 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 Widgets. 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 Widgets. *** ## Branding & customization [Section titled “Branding & customization”](#branding--customization) Hosted Widgets can be customized to match your application’s [branding](/fsa/guides/login-page-branding/). Hosted Widgets use your application logo, favicon, primary color, and more to look like an extension of your app. You can also change the Hosted Widgets URL to match your application URL by setting up a [custom domain](/guides/custom-domain/). ## Common Hosted Widgets scenarios [Section titled “Common Hosted Widgets scenarios”](#common-hosted-widgets-scenarios) What happens if a user does not have a session? If no session exists, the user is redirected automatically to the hosted login page of your application. What happens when a user logs out from Hosted Widgets? When a user logs out from Hosted Widgets, they are redirected to the hosted login page of your application. This can cause your app session and the Scalekit session to fall out of sync. We recommend one of the following approaches: * Implementing [back-channel logout](/guides/dashboard/redirects/#back-channel-logout-url) so Scalekit can notify your app about session termination. * Listening for the [user logout webhook](/apis/#webhook/userlogout) to get notified about session termination. --- # 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 ## 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/auth-methods/enterprise-sso/). 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. ![](/.netlify/images?url=_astro%2Fjit-provisioning.CWBROiBA.png\&w=2934\&h=1588\&dpl=69cce21a4f77360008b1503a) 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. ![](/.netlify/images?url=_astro%2Fsync-user-profile.DW9qgfGm.png\&w=2932\&h=1580\&dpl=69cce21a4f77360008b1503a) 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](/guides/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/auth-methods/enterprise-sso/). 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/auth-methods/enterprise-sso/) 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. 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. 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 --- # 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. ![](/.netlify/images?url=_astro%2F1-switcher.BmXDeGKX.png\&w=2940\&h=1662\&dpl=69cce21a4f77360008b1503a) ## 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 1 // Use case: Show organization switcher after user authentication 2 const redirectUri = 'http://localhost:3000/api/callback'; 3 const options = { 4 scopes: ['openid', 'profile', 'email', 'offline_access'], 5 prompt: 'select_account' 6 }; 7 8 const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options); 9 10 res.redirect(authorizationUrl); ``` * Python Flask ```diff 1 # Use case: Show organization switcher after user authentication 2 from scalekit import AuthorizationUrlOptions 3 4 redirect_uri = 'http://localhost:3000/api/callback' 5 options = AuthorizationUrlOptions() 6 options.scopes = ['openid', 'profile', 'email', 'offline_access'] 7 options.prompt = 'select_account' 8 9 authorization_url = scalekit.get_authorization_url(redirect_uri, options) 10 return redirect(authorization_url) ``` * Go Gin ```diff 1 // Use case: Show organization switcher after user authentication 2 redirectUri := "http://localhost:3000/api/callback" 3 options := scalekit.AuthorizationUrlOptions{ 4 Scopes: []string{"openid", "profile", "email", "offline_access"}, 5 +Prompt: "select_account", 6 } 7 8 authorizationUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options) 9 if err != nil { 10 // handle error appropriately 11 panic(err) 12 } 13 14 c.Redirect(http.StatusFound, authorizationUrl.String()) ``` * Java Spring ```diff 1 // Use case: Show organization switcher after user authentication 2 import com.scalekit.internal.http.AuthorizationUrlOptions; 3 import java.net.URL; 4 import java.util.Arrays; 5 6 String redirectUri = "http://localhost:3000/api/callback"; 7 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 8 +options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access")); 9 options.setPrompt("select_account"); 10 11 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 1 // Use case: Directly route users to a specific organization 2 const redirectUri = 'http://localhost:3000/api/callback'; 3 const options = { 4 scopes: ['openid', 'profile', 'email', 'offline_access'], 5 prompt: 'select_account', 6 organizationId: 'org_1233434' 7 }; 8 9 const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options); 10 11 res.redirect(authorizationUrl); ``` * Python Flask ```diff 1 # Use case: Directly route users to a specific organization 2 from scalekit import AuthorizationUrlOptions 3 4 redirect_uri = 'http://localhost:3000/api/callback' 5 options = AuthorizationUrlOptions() 6 options.scopes = ['openid', 'profile', 'email', 'offline_access'] 7 options.prompt = 'select_account' 8 options.organization_id = 'org_1233434' 9 10 authorization_url = scalekit.get_authorization_url(redirect_uri, options) 11 return redirect(authorization_url) ``` * Go Gin ```diff 1 // Use case: Directly route users to a specific organization 2 redirectUri := "http://localhost:3000/api/callback" 3 options := scalekit.AuthorizationUrlOptions{ 4 +Scopes: []string{"openid", "profile", "email", "offline_access"}, 5 +Prompt: "select_account", 6 OrganizationId: "org_1233434", 7 } 8 9 authorizationUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options) 10 if err != nil { 11 // handle error appropriately 12 panic(err) 13 } 14 15 c.Redirect(http.StatusFound, authorizationUrl.String()) ``` * Java Spring ```diff 1 // Use case: Directly route users to a specific organization 2 import com.scalekit.internal.http.AuthorizationUrlOptions; 3 import java.net.URL; 4 import java.util.Arrays; 5 6 String redirectUri = "http://localhost:3000/api/callback"; 7 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 8 +options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access")); 9 +options.setPrompt("select_account"); 10 options.setOrganizationId("org_1233434"); 11 12 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 ### 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**. 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](/guides/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 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 Servers - Additional Reading > Explore advanced topics for MCP servers, including OAuth 2.1 flows, scope design, dynamic client registration, and security best practices. MCP Clients that want to get authorized to access your MCP Server need to follow either of the below OAuth 2.1 Flows Supported by Scalekit. ## OAuth 2.1 Flows Supported [Section titled “OAuth 2.1 Flows Supported”](#oauth-21-flows-supported) ### Authorization Code Flow [Section titled “Authorization Code Flow”](#authorization-code-flow) Ideal when an AI agent or MCP Client acts on behalf of a human user: ```javascript 1 // Step 1: Redirect user to authorization server 2 const authURL = new URL('https://your-org.scalekit.com/oauth/authorize'); 3 authURL.searchParams.set('response_type', 'code'); 4 authURL.searchParams.set('client_id', 'your-client-id'); 5 authURL.searchParams.set('redirect_uri', 'https://your-app.com/callback'); 6 authURL.searchParams.set('scope', 'mcp:tools:calendar:read mcp:tools:email:send'); 7 authURL.searchParams.set('state', generateSecureRandomString()); 8 authURL.searchParams.set('code_challenge', generatePKCEChallenge()); 9 authURL.searchParams.set('code_challenge_method', 'S256'); 10 11 // Step 2: Handle callback and exchange code for token 12 app.get('/callback', async (req, res) => { 13 const { code, state } = req.query; 14 15 // Verify state parameter to prevent CSRF 16 if (!isValidState(state)) { 17 return res.status(400).json({ error: 'Invalid state parameter' }); 18 } 19 20 const tokenResponse = await fetch('https://your-org.scalekit.com/oauth/token', { 21 method: 'POST', 22 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 23 body: new URLSearchParams({ 24 grant_type: 'authorization_code', 25 code, 26 client_id: 'your-client-id', 27 redirect_uri: 'https://your-app.com/callback', 28 code_verifier: getPKCEVerifier() // From PKCE challenge generation 29 }) 30 }); 31 32 const tokens = await tokenResponse.json(); 33 // Store tokens securely and proceed with MCP calls 34 }); ``` ### Client Credentials Flow [Section titled “Client Credentials Flow”](#client-credentials-flow) Perfect for automated agents that don’t represent a specific user but want to access your MCP Server on their own behalf. This is typically used for Machine-to-Machine (M2M) authentication. ```javascript 1 const getMachineToken = async () => { 2 const response = await fetch('https://your-org.scalekit.com/oauth/token', { 3 method: 'POST', 4 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 5 body: new URLSearchParams({ 6 grant_type: 'client_credentials', 7 client_id: 'your-service-client-id', 8 client_secret: 'your-service-client-secret', 9 scope: 'mcp:tools:inventory:check mcp:resources:store-data', 10 audience: 'https://your-mcp-server.com', 11 }) 12 }); 13 14 return await response.json(); 15 }; ``` ## Scope Design Best Practices [Section titled “Scope Design Best Practices”](#scope-design-best-practices) Design OAuth scopes that reflect your MCP server’s actual capabilities and security requirements: ### Hierarchical Scopes [Section titled “Hierarchical Scopes”](#hierarchical-scopes) ```javascript 1 // Resource-based scopes 2 'mcp:resources:customer-data:read' // Read customer data 3 'mcp:resources:customer-data:write' // Modify customer data 4 'mcp:resources:*' // All resources (admin-level) 5 6 // Tool-based scopes 7 'mcp:tools:weather' // Weather API access 8 'mcp:tools:calendar:read' // Read calendar events 9 'mcp:tools:calendar:write' // Create/modify calendar events 10 'mcp:tools:email:send' // Send emails 11 'mcp:tools:*' // All tools access 12 13 // Action-based scopes 14 'mcp:exec:workflows:risk-assessment' // Execute risk assessment workflow 15 'mcp:exec:functions:data-analysis' // Run data analysis functions ``` ### Scope Validation Helpers [Section titled “Scope Validation Helpers”](#scope-validation-helpers) ```javascript 1 const ScopeValidator = { 2 hasScope: (userScopes, requiredScope) => { 3 return userScopes.includes(requiredScope) || 4 userScopes.includes(requiredScope.split(':').slice(0, -1).join(':') + ':*'); 5 }, 6 7 hasAnyScope: (userScopes, allowedScopes) => { 8 return allowedScopes.some(scope => ScopeValidator.hasScope(userScopes, scope)); 9 }, 10 11 validateToolAccess: (userScopes, toolName) => { 12 const toolScope = `mcp:tools:${toolName}`; 13 const wildcardScope = 'mcp:tools:*'; 14 return userScopes.includes(toolScope) || userScopes.includes(wildcardScope); 15 } 16 }; 17 18 // Usage in MCP tool handlers 19 app.post('/mcp/tools/:toolName', (req, res) => { 20 const { toolName } = req.params; 21 const userScopes = req.auth.scopes; 22 23 if (!ScopeValidator.validateToolAccess(userScopes, toolName)) { 24 return res.status(403).json({ 25 error: 'insufficient_scope', 26 error_description: `Access to tool '${toolName}' requires appropriate scope` 27 }); 28 } 29 30 // Process tool request 31 }); ``` ## Dynamic Client Registration [Section titled “Dynamic Client Registration”](#dynamic-client-registration) Scalekit supports Dynamic Client Registration (DCR) to enable seamless integration for new MCP clients that want to connect to your MCP Server. MCP clients can auto-register using DCR: ```javascript 1 // MCP clients can auto-register using DCR 2 const registerClient = async (clientMetadata) => { 3 const response = await fetch('https://your-org.scalekit.com/resource-server/oauth/register', { 4 method: 'POST', 5 headers: { 'Content-Type': 'application/json' }, 6 body: JSON.stringify({ 7 client_name: 'AI Sales Assistant', 8 client_uri: 'https://sales-ai.company.com', 9 redirect_uris: ['https://sales-ai.company.com/oauth/callback'], 10 grant_types: ['authorization_code', 'refresh_token'], 11 response_types: ['code'], 12 scope: 'mcp:tools:crm:read mcp:tools:email:send', 13 audience: 'https://your-mcp-server.com', 14 token_endpoint_auth_method: 'client_secret_basic', 15 ...clientMetadata 16 }) 17 }); 18 19 return await response.json(); 20 // Returns: { client_id, client_secret, client_id_issued_at, ... } 21 }; ``` ## Security Implementation [Section titled “Security Implementation”](#security-implementation) ### Rate Limiting by Client [Section titled “Rate Limiting by Client”](#rate-limiting-by-client) Implement client-specific rate limits: ```javascript 1 import rateLimit from 'express-rate-limit'; 2 3 const createClientRateLimit = () => { 4 return rateLimit({ 5 windowMs: 15 * 60 * 1000, // 15 minutes 6 limit: (req) => { 7 // Different limits based on client type or scopes 8 const scopes = req.auth?.scopes || []; 9 if (scopes.includes('mcp:tools:*')) return 1000; // Premium client 10 if (scopes.includes('mcp:tools:basic')) return 100; // Basic client 11 return 50; // Default limit 12 }, 13 keyGenerator: (req) => req.auth?.clientId || req.ip, 14 message: { 15 error: 'rate_limit_exceeded', 16 error_description: 'Too many requests from this client' 17 } 18 }); 19 }; 20 21 app.use('/mcp', createClientRateLimit()); ``` ### Comprehensive Logging [Section titled “Comprehensive Logging”](#comprehensive-logging) Track all OAuth and MCP interactions: ```javascript 1 const auditLogger = { 2 logTokenRequest: (clientId, grantType, scopes, success) => { 3 console.log(JSON.stringify({ 4 event: 'oauth_token_request', 5 timestamp: new Date().toISOString(), 6 client_id: clientId, 7 grant_type: grantType, 8 requested_scopes: scopes, 9 success 10 })); 11 }, 12 13 logMCPAccess: (req, toolName, success, error = null) => { 14 console.log(JSON.stringify({ 15 event: 'mcp_tool_access', 16 timestamp: new Date().toISOString(), 17 user_id: req.auth?.userId, 18 client_id: req.auth?.clientId, 19 tool_name: toolName, 20 scopes: req.auth?.scopes, 21 success, 22 error: error?.message, 23 ip_address: req.ip, 24 user_agent: req.get('User-Agent') 25 })); 26 } 27 }; 28 29 // Use in your MCP handlers 30 app.post('/mcp/tools/:toolName', async (req, res) => { 31 const { toolName } = req.params; 32 33 try { 34 // Process tool request 35 const result = await processToolRequest(toolName, req.body); 36 37 auditLogger.logMCPAccess(req, toolName, true); 38 res.json(result); 39 } catch (error) { 40 auditLogger.logMCPAccess(req, toolName, false, error); 41 res.status(500).json({ error: 'Tool execution failed' }); 42 } 43 }); ``` ### Health Check Endpoints [Section titled “Health Check Endpoints”](#health-check-endpoints) Monitor your MCP server and authorization integration: ```javascript 1 app.get('/health', async (req, res) => { 2 const health = { 3 status: 'healthy', 4 timestamp: new Date().toISOString(), 5 services: { 6 mcp_server: 'healthy', 7 oauth_server: 'unknown' 8 } 9 }; 10 11 try { 12 // Test OAuth server connectivity 13 const oauthTest = await fetch('https://your-org.scalekit.com/.well-known/oauth-authorization-server'); 14 health.services.oauth_server = oauthTest.ok ? 'healthy' : 'degraded'; 15 } catch (error) { 16 health.services.oauth_server = 'unhealthy'; 17 health.status = 'degraded'; 18 } 19 20 const statusCode = health.status === 'healthy' ? 200 : 503; 21 res.status(statusCode).json(health); 22 }); ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Common Issues and Solutions [Section titled “Common Issues and Solutions”](#common-issues-and-solutions) **Token Validation Failures** ```javascript 1 // Debug token validation issues 2 const debugTokenValidation = async (token) => { 3 try { 4 // Check token structure 5 const [header, payload, signature] = token.split('.'); 6 console.log('Token Header:', JSON.parse(atob(header))); 7 console.log('Token Payload:', JSON.parse(atob(payload))); 8 9 // Validate with detailed error info 10 await jwtVerify(token, JWKS, { 11 issuer: 'https://your-org.scalekit.com', 12 audience: 'https://your-mcp-server.com' 13 }); 14 } catch (error) { 15 console.error('Token validation error:', { 16 name: error.name, 17 message: error.message, 18 code: error.code 19 }); 20 } 21 }; ``` **CORS Issues with Authorization Server** ```javascript 1 // Configure CORS for OAuth endpoints 2 app.use('/oauth', cors({ 3 origin: 'https://your-org.scalekit.com', 4 credentials: true, 5 methods: ['GET', 'POST', 'OPTIONS'], 6 allowedHeaders: ['Authorization', 'Content-Type', 'MCP-Protocol-Version'] 7 })); ``` **Scope Permission Debugging** ```javascript 1 const debugScopes = (req, res, next) => { 2 console.log('Request Scopes:', { 3 user_scopes: req.auth?.scopes, 4 required_scope: req.requiredScope, 5 has_permission: req.auth?.scopes?.includes(req.requiredScope) 6 }); 7 next(); 8 }; ``` ### Error Response Standards [Section titled “Error Response Standards”](#error-response-standards) Follow OAuth 2.1 and MCP error response formats: ```javascript 1 const sendOAuthError = (res, error, description, statusCode = 400) => { 2 res.status(statusCode).json({ 3 error, 4 error_description: description, 5 error_uri: 'https://your-mcp-server.com/docs/errors' 6 }); 7 }; 8 9 // Usage examples 10 app.use((error, req, res, next) => { 11 if (error.name === 'TokenExpiredError') { 12 return sendOAuthError(res, 'invalid_token', 'Access token has expired', 401); 13 } 14 15 if (error.name === 'InsufficientScopeError') { 16 return sendOAuthError(res, 'insufficient_scope', `Required scope: ${error.requiredScope}`, 403); 17 } 18 19 // Default error 20 sendOAuthError(res, 'server_error', 'An unexpected error occurred', 500); 21 }); ``` ## Advanced Configuration [Section titled “Advanced Configuration”](#advanced-configuration) ### Custom Scope Mapping [Section titled “Custom Scope Mapping”](#custom-scope-mapping) Map OAuth scopes to internal permissions: ```javascript 1 const scopePermissionMap = { 2 'mcp:tools:weather': ['weather:read'], 3 'mcp:tools:calendar:read': ['calendar:events:read'], 4 'mcp:tools:calendar:write': ['calendar:events:read', 'calendar:events:write'], 5 'mcp:tools:email:send': ['email:send', 'contacts:read'], 6 'mcp:resources:customer-data': ['customers:read', 'customers:write'] 7 }; 8 9 const getPermissionsFromScopes = (scopes) => { 10 const permissions = new Set(); 11 scopes.forEach(scope => { 12 const scopePermissions = scopePermissionMap[scope] || []; 13 scopePermissions.forEach(permission => permissions.add(permission)); 14 }); 15 return Array.from(permissions); 16 }; ``` ### Refresh Token Management [Section titled “Refresh Token Management”](#refresh-token-management) Handle token refresh for long-running agents: ```javascript 1 const TokenManager = { 2 async refreshToken(refreshToken) { 3 const response = await fetch('https://your-org.scalekit.com/oauth2/token', { 4 method: 'POST', 5 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 6 body: new URLSearchParams({ 7 grant_type: 'refresh_token', 8 refresh_token: refreshToken, 9 client_id: 'your-client-id', 10 client_secret: 'your-client-secret' 11 }) 12 }); 13 14 return await response.json(); 15 }, 16 17 async autoRefreshWrapper(tokenStore, makeRequest) { 18 try { 19 return await makeRequest(tokenStore.accessToken); 20 } catch (error) { 21 if (error.status === 401) { 22 // Token expired, try refresh 23 const newTokens = await this.refreshToken(tokenStore.refreshToken); 24 tokenStore.accessToken = newTokens.access_token; 25 tokenStore.refreshToken = newTokens.refresh_token; 26 27 // Retry original request 28 return await makeRequest(tokenStore.accessToken); 29 } 30 throw error; 31 } 32 } 33 }; ``` --- # DOCUMENT BOUNDARY --- # MCP authentication patterns > Authentication patterns: Human users via OAuth Authorization Code flow, autonomous agents via Client Credentials flow, and downstream integrations using API keys, OAuth, or token cascading Scalekit provides secure authentication for MCP servers across three distinct patterns, each corresponding to different interaction models and trust boundaries. Understanding which pattern applies to your use case ensures you implement the right security model for your MCP server architecture. This guide covers all three authentication patterns: human-to-MCP interactions, agent-to-MCP communication, and MCP-to-downstream integrations. Each pattern uses different OAuth 2.1 flows and has specific configuration requirements explained with sequence diagrams and practical guidance. ## Pattern comparison [Section titled “Pattern comparison”](#pattern-comparison) Understanding the differences between these patterns helps you choose the right approach for your architecture. Each pattern serves specific use cases and has different security characteristics. | Aspect | Human → MCP | Agent/Machine → MCP | MCP → Downstream | | -------------------- | ---------------------------------------------- | -------------------------------------- | -------------------------------------- | | **Actor** | Human using AI host (Claude, ChatGPT, VS Code) | Autonomous agent or service | MCP Server making backend calls | | **OAuth Flow** | Authorization Code | Client Credentials | Varies by sub-pattern | | **Initiator** | User interaction in MCP client | Programmatic request | MCP server implementation code | | **Token Lifetime** | Medium (typically hours) | Configurable (typically long-lived) | Depends on downstream system | | **User Consent** | Required during authorization flow | Not applicable (pre-configured) | Not applicable | | **Scope Assignment** | During consent prompt | At client registration | At implementation time | | **Best For** | Interactive human workflows | Scheduled tasks, autonomous operations | Backend integration with APIs/services | | **Complexity** | Medium (handles browser flow) | Low (direct token request) | Varies (simple to complex) | ## Pattern 1: Human interacting with MCP server [Section titled “Pattern 1: Human interacting with MCP server”](#pattern-1-human-interacting-with-mcp-server) When a human uses a compliant MCP host application, 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 pattern represents the most common interaction model for real-world MCP use cases - humans interacting with an MCP server through AI host applications like Claude Desktop, VS Code, Cursor, or Windsurf, while Scalekit ensures tokens are valid, scoped, and auditable. OAuth flow summary Human-initiated MCP interactions use the **OAuth 2.1 Authorization Code Flow**. Scalekit acts as the Authorization Server, the MCP Server as the Protected Resource, and the AI host (ChatGPT, Claude, Windsurf, etc.) as the OAuth Client. ### Authorization sequence [Section titled “Authorization sequence”](#authorization-sequence) ### How it works [Section titled “How it works”](#how-it-works) 1. **Initiation** – The human configures an MCP server in their MCP client application. 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 Magic Link & OTP, 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. ### Implementation [Section titled “Implementation”](#implementation) #### 1. Register your MCP server in the Scalekit Dashboard [Section titled “1. Register your MCP server in the Scalekit Dashboard”](#1-register-your-mcp-server-in-the-scalekit-dashboard) Create a new MCP server in the Scalekit Dashboard to obtain your server credentials and configure authentication settings. #### 2. Implement the protected resource metadata endpoint [Section titled “2. Implement the protected resource metadata endpoint”](#2-implement-the-protected-resource-metadata-endpoint) Add a `.well-known/oauth-protected-resource` endpoint that provides your MCP server’s authentication configuration to clients. #### 3. Configure scopes for your server capabilities [Section titled “3. Configure scopes for your server capabilities”](#3-configure-scopes-for-your-server-capabilities) Define OAuth scopes that correspond to the tools and permissions your MCP server exposes. #### 4. Set up token validation middleware [Section titled “4. Set up token validation middleware”](#4-set-up-token-validation-middleware) Implement middleware to validate incoming JWT tokens from Scalekit before processing MCP tool requests. #### 5. Test the complete authentication flow [Section titled “5. Test the complete authentication flow”](#5-test-the-complete-authentication-flow) Verify the end-to-end flow works with an MCP client to ensure secure authentication. For complete implementation guidance, see the [MCP OAuth 2.1 quickstart](/authenticate/mcp/quickstart/) or framework-specific guides for [FastMCP](/authenticate/mcp/fastmcp-quickstart/), [FastAPI + FastMCP](/authenticate/mcp/fastapi-fastmcp-quickstart/), and [Express.js](/authenticate/mcp/expressjs-quickstart/). ## Pattern 2: Agent / machine interacting with MCP server [Section titled “Pattern 2: Agent / machine interacting with MCP server”](#pattern-2-agent--machine-interacting-with-mcp-server) 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 pattern 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. OAuth 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-1) ### Client registration [Section titled “Client registration”](#client-registration) #### 1. Navigate to the MCP Server Clients tab [Section titled “1. Navigate to the MCP Server Clients tab”](#1-navigate-to-the-mcp-server-clients-tab) Go to **[Dashboard](https://app.scalekit.com) > MCP Servers** and select your MCP Server. Click on the **Clients** tab. ![Clients tab](/.netlify/images?url=_astro%2Fmcp-client-nav.C6UPUhIu.png\&w=1148\&h=1242\&dpl=69cce21a4f77360008b1503a) #### 2. Create a new M2M client [Section titled “2. Create a new M2M client”](#2-create-a-new-m2m-client) Click **Create Client** to start the client creation process. ![Create client](/.netlify/images?url=_astro%2Fmcp-clients-tab.UgPaVUGm.png\&w=3020\&h=1040\&dpl=69cce21a4f77360008b1503a) #### 3. Copy your client credentials [Section titled “3. Copy your client credentials”](#3-copy-your-client-credentials) Copy the **client\_id** and **client\_secret** immediately - the secret will not be shown again for security reasons. Store these securely in your agent’s configuration. ![Client credentials](/.netlify/images?url=_astro%2Fmcp-client-sidesheet.D9KN4b5q.png\&w=3020\&h=1500\&dpl=69cce21a4f77360008b1503a) #### 4. Configure client scopes [Section titled “4. Configure client scopes”](#4-configure-client-scopes) Optionally, set scopes (e.g., `todo:read`, `todo:write`) that correspond to the permissions configured for your MCP Server. Click **Save** to complete the setup. ### 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: Request access token ```bash 1 curl --location '{{env_url}}/oauth/token' \ 2 --header 'Content-Type: application/x-www-form-urlencoded' \ 3 --data-urlencode 'grant_type=client_credentials' \ 4 --data-urlencode 'client_id={{client_id}}' \ 5 --data-urlencode 'client_secret={{secret_value}}' \ 6 --data-urlencode 'scope=todo:read todo:write' ``` Scalekit responds with a JSON payload containing the access token: Token response ```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 endpoints. Token caching best practice 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. ### Implementation [Section titled “Implementation”](#implementation-1) #### 1. Create an M2M client for your target MCP server [Section titled “1. Create an M2M client for your target MCP server”](#1-create-an-m2m-client-for-your-target-mcp-server) Use the Scalekit Dashboard to create a Machine-to-Machine client for the MCP server you want to authenticate with. #### 2. Store client credentials securely [Section titled “2. Store client credentials securely”](#2-store-client-credentials-securely) Store the `client_id` and `client_secret` using environment variables or a secrets manager. Never hardcode credentials in your agent code. #### 3. Implement token requests in your agent [Section titled “3. Implement token requests in your agent”](#3-implement-token-requests-in-your-agent) Before making MCP calls, request access tokens using the OAuth 2.1 Client Credentials flow from the Scalekit Authorization Server. #### 4. Add token caching and refresh logic [Section titled “4. Add token caching and refresh logic”](#4-add-token-caching-and-refresh-logic) Implement caching to store tokens until they expire, and refresh them automatically to maintain uninterrupted service. #### 5. Attach tokens to MCP tool requests [Section titled “5. Attach tokens to MCP tool requests”](#5-attach-tokens-to-mcp-tool-requests) Include the access token as a Bearer token in the `Authorization` header when calling MCP server tools. For hands-on experience, use the FastMCP Todo Server from the [FastMCP quickstart](/authenticate/mcp/fastmcp-quickstart/). Create an M2M client and run your token request programmatically within your agent code. ## Pattern 3: MCP server integrating with downstream systems [Section titled “Pattern 3: MCP server integrating with downstream systems”](#pattern-3-mcp-server-integrating-with-downstream-systems) 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 section explains three secure ways to perform these downstream integrations, each corresponding to a different trust boundary and authorization pattern. ### Sub-pattern 3a: Using API keys or custom tokens [Section titled “Sub-pattern 3a: Using API keys or custom tokens”](#sub-pattern-3a-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, in environment variables, a vault, or secrets manager) and injects them when making downstream calls. Security 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. #### Authorization sequence [Section titled “Authorization sequence”](#authorization-sequence-2) #### When to use this pattern [Section titled “When to use this pattern”](#when-to-use-this-pattern) * External APIs have their own authentication (AWS, Stripe, Twilio, etc.) * Internal systems use proprietary authentication mechanisms * Legacy systems that don’t support OAuth 2.1 * You control credential management and rotation #### Example scenario [Section titled “Example scenario”](#example-scenario) * 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 headers * The backend API validates the key and responds with data * The MCP Server processes and returns the formatted response to the client ### Sub-pattern 3b: MCP-to-MCP communication [Section titled “Sub-pattern 3b: MCP-to-MCP communication”](#sub-pattern-3b-mcp-to-mcp-communication) 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 **Pattern 2** above. 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. #### Authorization sequence [Section titled “Authorization sequence”](#authorization-sequence-3) #### Implementation [Section titled “Implementation”](#implementation-2) The implementation follows Pattern 2 (Agent/Machine → MCP): 1. Create an M2M client for the receiving MCP server in Scalekit 2. Configure the calling MCP server with the client credentials 3. Request tokens using the Client Credentials flow 4. Call the receiving MCP’s tools with the Bearer token For detailed implementation guidance, refer to the [Pattern 2 section](#pattern-2-agent--machine-interacting-with-mcp-server) above. ### Sub-pattern 3c: Cascading the same token [Section titled “Sub-pattern 3c: Cascading the same token”](#sub-pattern-3c-cascading-the-same-token) 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. #### Authorization sequence [Section titled “Authorization sequence”](#authorization-sequence-4) #### When to use this pattern [Section titled “When to use this pattern”](#when-to-use-this-pattern-1) Use token cascading when: * Both systems (MCP Server and backend 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 * You need to preserve the original user context across service boundaries Trust boundary consideration Only cascade tokens across services that share the same trust boundary. If your backend API does not validate Scalekit-issued tokens, use a separate service credential or the Client Credentials flow (sub-pattern 3b) instead. #### Implementation requirements [Section titled “Implementation requirements”](#implementation-requirements) For the backend API to validate cascaded tokens: 1. Configure the backend to validate JWT signatures using Scalekit’s public keys 2. Verify the token’s `iss` (issuer) claim matches your Scalekit environment 3. Check the `aud` (audience) claim includes the backend API’s identifier 4. Validate the `exp` (expiration) claim to reject expired tokens 5. Verify required scopes are present in the token’s `scope` claim ## Choosing the right pattern [Section titled “Choosing the right pattern”](#choosing-the-right-pattern) Use this decision guide to select the appropriate authentication pattern for your use case: **For human users accessing MCP tools:** → Use **Pattern 1: Human → MCP** (Authorization Code Flow) **For autonomous agents or scheduled tasks:** → Use **Pattern 2: Agent/Machine → MCP** (Client Credentials Flow) **For MCP server making backend calls:** * External APIs with their own auth → Use **Pattern 3a: API Keys** * Another MCP server you control → Use **Pattern 3b: MCP-to-MCP** (Client Credentials Flow) * Backend within same trust boundary → Use **Pattern 3c: Token Cascading** ## Next steps [Section titled “Next steps”](#next-steps) Now that you understand the authentication patterns, you can: * Follow the [MCP OAuth 2.1 quickstart](/authenticate/mcp/quickstart/) to implement Pattern 1 or Pattern 2 * Explore framework-specific implementations: * [FastMCP quickstart](/authenticate/mcp/fastmcp-quickstart/) for Python with built-in provider * [FastAPI + FastMCP quickstart](/authenticate/mcp/fastapi-fastmcp-quickstart/) for custom Python middleware * [Express.js quickstart](/authenticate/mcp/expressjs-quickstart/) for Node.js/TypeScript servers * Review the [MCP authentication demos](https://github.com/scalekit-inc/mcp-auth-demos) on GitHub for complete working examples --- # 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 --- # Bring your own auth into your MCP server > Federated authentication system with Scalekit's OAuth 2.1 authorization layer for MCP servers If you already have an authentication system in place, you can use Scalekit as a drop-in OAuth 2.1 authorization layer for your MCP servers. This federated approach allows you to maintain your existing auth infrastructure while adding standards-compliant OAuth 2.1 authorization for MCP clients. **Why use federated authentication?** * **Preserve existing auth**: Keep your current authentication system and user management * **Standards compliance**: Add OAuth 2.1 authorization without rebuilding your auth layer * **Seamless integration**: Users authenticate with your familiar login experience * **Centralized control**: Maintain full control over user authentication and policies When an MCP client initiates authentication, Scalekit acts as a bridge between the MCP client and your existing authentication system. The flow involves redirecting users to your login endpoint, validating their identity, and passing user information back to Scalekit to complete the OAuth 2.1 flow. 1. ## Initiate authentication flow [Section titled “Initiate authentication flow”](#initiate-authentication-flow) When the MCP client starts the authentication flow by calling `/oauth/authorize` on Scalekit, Scalekit redirects the user to your configured login endpoint with two critical parameters: * `login_request_id` string : Unique identifier for this login request * `state` string : OAuth state parameter to maintain security across requests **Example redirect URL:** ```sh https:///login?login_request_id=&state= ``` 2. ## Authenticate the user in your system [Section titled “Authenticate the user in your system”](#authenticate-the-user-in-your-system) When the user lands on your login page, process authentication using your existing logic?whether that’s username/password, SSO, biometric authentication, or any other method your system supports. After successful authentication, make a secure backend-to-backend POST request to Scalekit with the authenticated user’s information. Send user details to Scalekit ```bash curl --location '/api/v1/connections//auth-requests//user' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data-raw '{ "sub": "1234567890", "email": "alice@example.com", "given_name": "Alice", "family_name": "Doe", "email_verified": true, "phone_number": "+1234567890", "phone_number_verified": false, "name": "Alice Doe", "preferred_username": "alice.d", "picture": "https://example.com/avatar.jpg", "gender": "female", "locale": "en-US" }' ``` User attribute descriptions **Required attributes:** * `sub` string ? Unique identifier for the user in your system (subject) * `email` string ? User’s email address **Optional attributes:** * `given_name` string ? User’s first name * `family_name` string ? User’s last name * `email_verified` boolean ? Whether email has been verified * `phone_number` string ? User’s phone number in E.164 format * `phone_number_verified` boolean ? Whether phone has been verified * `name` string ? User’s full name * `preferred_username` string ? Preferred username * `picture` string ? URL to user’s profile picture * `gender` string ? User’s gender * `locale` string ? User’s locale preference (e.g., “en-US”) Note Replace the placeholder values: * `` ? Your Scalekit environment URL * `` ? Unique connection ID provided by Scalekit for your auth integration * `` ? The login request ID from step 1 * `` ? Your Scalekit API access token 3. ## Redirect back to Scalekit [Section titled “Redirect back to Scalekit”](#redirect-back-to-scalekit) After receiving a successful response from Scalekit confirming the user details were accepted, redirect the user back to Scalekit’s callback endpoint with the `state` parameter. **Callback URL format:** ```sh /sso/v1/connections//partner:callback?state= ``` The `state_value` must match the `state` parameter you received in step 1. This ensures the authentication flow’s integrity and prevents CSRF attacks. State validation Always verify that the `state` value you send back matches exactly what you received initially. Mismatched state values should be rejected. 4. ## Complete the OAuth flow [Section titled “Complete the OAuth flow”](#complete-the-oauth-flow) After processing the callback from your authentication system, Scalekit automatically handles the remaining OAuth 2.1 flow steps: * Displays the consent screen to the user (if required) * Generates the authorization code * Handles token exchange requests from the MCP client * Issues access tokens with appropriate scopes The MCP client receives valid OAuth 2.1 tokens and can now access your MCP server with the authenticated user’s identity. Security best practices * Store and transmit all sensitive data (tokens, user information) securely * Use HTTPS for all communications between your system and Scalekit * Implement proper logging for authentication events for audit trails * The `login_request_id` and `state` parameters are critical for security?never reuse them across requests Your MCP server now supports federated authentication with your existing auth system --- # 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 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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 1 mkdir express-mcp-node 2 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 1 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 1 cat <<'EOF' > .env 2 PORT=3002 3 SK_ENV_URL=https://.scalekit.com 4 SK_CLIENT_ID= 5 SK_CLIENT_SECRET= 6 MCP_SERVER_ID= 7 PROTECTED_RESOURCE_METADATA='' 8 EXPECTED_AUDIENCE=http://localhost:3002/ 9 EOF 10 11 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 from **Dashboard > MCP Servers**. Not directly used in this implementation but documented for reference. | | `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) 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 EXPECTED_AUDIENCE = process.env.EXPECTED_AUDIENCE ?? ''; 15 const PROTECTED_RESOURCE_METADATA = process.env.PROTECTED_RESOURCE_METADATA ?? ''; 16 17 // Use case: Configure OAuth resource metadata URL for MCP clients 18 // This allows MCP clients to discover authorization requirements via WWW-Authenticate header 19 // Security: The WWW-Authenticate header signals to clients where to obtain tokens 20 const RESOURCE_METADATA_URL = `http://localhost:${PORT}/.well-known/oauth-protected-resource`; 21 22 // WWW-Authenticate header for 401 responses 23 const WWW_HEADER_KEY = 'WWW-Authenticate'; 24 const WWW_HEADER_VALUE = `Bearer realm="OAuth", resource_metadata="${RESOURCE_METADATA_URL}"`; 25 26 // Initialize Scalekit client for token validation 27 // Security: Use SDK to validate JWT signatures and claims 28 // This prevents accepting forged or tampered tokens 29 const scalekit = new Scalekit(SK_ENV_URL, SK_CLIENT_ID, SK_CLIENT_SECRET); 30 31 // Initialize MCP server with greeting tool 32 // Context: The McpServer handles MCP protocol details while Express handles HTTP routing 33 const server = new McpServer({ name: 'Greeting MCP', version: '1.0.0' }); 34 35 // Use case: Simple greeting tool demonstrating OAuth-protected MCP operations 36 // Context: This tool is protected by the authentication middleware applied to all routes 37 server.tool( 38 'greet_user', 39 'Greets the user with a personalized message.', 40 { 41 name: z.string().min(1, 'Name is required'), 42 }, 43 async ({ name }: { name: string }) => ({ 44 content: [ 45 { 46 type: 'text', 47 text: `Hi ${name}, welcome to Scalekit!` 48 } 49 ] 50 }) 51 ); 52 53 // Initialize Express application 54 const app = express(); 55 56 // Enable CORS for cross-origin MCP clients 57 // Use case: Allow MCP clients from different origins to connect 58 app.use(cors({ origin: true, credentials: false })); 59 60 // Parse JSON request bodies 61 // Context: MCP protocol uses JSON-RPC format 62 app.use(express.json()); 63 64 // Use case: Expose OAuth resource metadata for MCP client discovery 65 // This endpoint allows clients to discover authorization requirements and server capabilities 66 // Context: MCP clients use this metadata to initiate the OAuth flow 67 app.get('/.well-known/oauth-protected-resource', (_req: Request, res: Response) => { 68 if (!PROTECTED_RESOURCE_METADATA) { 69 res.status(500).json({ error: 'PROTECTED_RESOURCE_METADATA config missing' }); 70 return; 71 } 72 73 const metadata = JSON.parse(PROTECTED_RESOURCE_METADATA); 74 res.type('application/json').send(JSON.stringify(metadata, null, 2)); 75 }); 76 77 // Use case: Health check endpoint for monitoring and load balancers 78 // Context: Keep this separate from protected endpoints for deployment health checks 79 app.get('/health', (_req: Request, res: Response) => { 80 res.json({ status: 'healthy' }); 81 }); 82 83 // Security: Validate Bearer tokens on all protected endpoints 84 // Public endpoints (health, metadata) are exempt from authentication 85 // This prevents unauthorized access to MCP tools and operations 86 app.use(async (req: Request, res: Response, next: NextFunction) => { 87 // Allow public endpoints without authentication 88 // Use case: Health checks for monitoring; metadata for client discovery 89 if (req.path === '/.well-known/oauth-protected-resource' || req.path === '/health') { 90 next(); 91 return; 92 } 93 94 // Extract Bearer token from Authorization header 95 // Use case: OAuth 2.1 Bearer token format (RFC 6750) 96 // Security: Reject requests without valid Bearer token prefix 97 const header = req.headers.authorization; 98 const token = header?.startsWith('Bearer ') 99 ? header.slice('Bearer '.length).trim() 100 : undefined; 101 102 if (!token) { 103 res.status(401) 104 .set(WWW_HEADER_KEY, WWW_HEADER_VALUE) 105 .json({ error: 'Missing Bearer token' }); 106 return; 107 } 108 109 try { 110 // Validate token using Scalekit SDK 111 // Security: Verifies signature, expiration, issuer, and audience claims 112 // Context: This critical step prevents accepting tokens from other issuers 113 await scalekit.validateToken(token, { audience: [EXPECTED_AUDIENCE] }); 114 next(); 115 } catch (error) { 116 res.status(401) 117 .set(WWW_HEADER_KEY, WWW_HEADER_VALUE) 118 .json({ error: 'Token validation failed' }); 119 } 120 }); 121 122 // Handle MCP protocol requests at root path 123 // Use case: Process authenticated MCP tool requests using StreamableHTTPServerTransport 124 // Context: The transport layer handles MCP JSON-RPC communication 125 app.post('/', async (req: Request, res: Response) => { 126 const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); 127 await server.connect(transport); 128 129 try { 130 await transport.handleRequest(req, res, req.body); 131 } catch (error) { 132 res.status(500).json({ error: 'MCP transport error' }); 133 } 134 }); 135 136 // Start the Express server 137 app.listen(PORT, () => { 138 console.log(`MCP server running on http://localhost:${PORT}`); 139 }); ``` 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 1 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 1 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=69cce21a4f77360008b1503a) 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 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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 1 mkdir fastapi-mcp-python 2 cd fastapi-mcp-python 3 python3 -m venv .venv 4 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 1 cat <<'EOF' > requirements.txt 2 mcp>=1.0.0 3 fastapi>=0.104.0 4 fastmcp>=0.8.0 5 uvicorn>=0.24.0 6 pydantic>=2.5.0 7 python-dotenv>=1.0.0 8 httpx>=0.25.0 9 python-jose[cryptography]>=3.3.0 10 cryptography>=41.0.0 11 scalekit-sdk-python>=2.4.0 12 starlette>=0.27.0 13 EOF 14 15 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 1 cat <<'EOF' > .env 2 PORT=3002 3 SK_ENV_URL=https://.scalekit.com 4 SK_CLIENT_ID= 5 SK_CLIENT_SECRET= 6 MCP_SERVER_ID= 7 PROTECTED_RESOURCE_METADATA='' 8 EXPECTED_AUDIENCE=http://localhost:3002/ 9 EOF 10 11 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 from **Dashboard > MCP Servers**. Not directly used in this implementation but documented for reference. | | `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) 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 EXPECTED_AUDIENCE = os.getenv("EXPECTED_AUDIENCE", "") 18 PROTECTED_RESOURCE_METADATA = os.getenv("PROTECTED_RESOURCE_METADATA", "") 19 20 # Use case: Configure OAuth resource metadata URL for MCP clients 21 # This allows MCP clients to discover authorization requirements via WWW-Authenticate header 22 # Security: The WWW-Authenticate header signals to clients where to obtain tokens 23 RESOURCE_METADATA_URL = f"http://localhost:{PORT}/.well-known/oauth-protected-resource" 24 WWW_HEADER = { 25 "WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{RESOURCE_METADATA_URL}"' 26 } 27 28 # Initialize Scalekit client for token validation 29 # Security: Use SDK to validate JWT signatures and claims 30 # This prevents accepting forged or tampered tokens 31 scalekit_client = ScalekitClient( 32 env_url=SK_ENV_URL, 33 client_id=SK_CLIENT_ID, 34 client_secret=SK_CLIENT_SECRET, 35 ) 36 37 # Initialize FastMCP with stateless HTTP transport 38 # HTTP transport allows MCP clients to connect via standard OAuth flows 39 mcp = FastMCP("Greeting MCP", stateless_http=True) 40 41 42 @mcp.tool( 43 name="greet_user", 44 description="Greets the user with a personalized message." 45 ) 46 async def greet_user(name: str, ctx: Context | None = None) -> dict: 47 """ 48 Use case: Simple greeting tool demonstrating OAuth-protected MCP operations 49 Context: This tool is protected by the authentication middleware 50 """ 51 return { 52 "content": [ 53 { 54 "type": "text", 55 "text": f"Hi {name}, welcome to Scalekit!" 56 } 57 ] 58 } 59 60 61 # Mount FastMCP as a FastAPI application 62 # Context: This allows us to layer FastAPI middleware on top of FastMCP 63 mcp_app = mcp.http_app(path="/") 64 app = FastAPI(lifespan=mcp_app.lifespan) 65 66 # Enable CORS for cross-origin MCP clients 67 # Use case: Allow MCP clients from different origins to connect 68 app.add_middleware( 69 CORSMiddleware, 70 allow_origins=["*"], 71 allow_credentials=True, 72 allow_methods=["GET", "POST", "OPTIONS"], 73 allow_headers=["*"] 74 ) 75 76 77 @app.middleware("http") 78 async def auth_middleware(request: Request, call_next): 79 """ 80 Security: Validate Bearer tokens on all protected endpoints. 81 Public endpoints (health, metadata) are exempt from authentication. 82 This prevents unauthorized access to MCP tools and operations. 83 """ 84 # Allow public endpoints without authentication 85 # Use case: Health checks for monitoring; metadata for client discovery 86 if request.url.path in {"/health", "/.well-known/oauth-protected-resource"}: 87 return await call_next(request) 88 89 # Extract Bearer token from Authorization header 90 # Use case: OAuth 2.1 Bearer token format (RFC 6750) 91 # Security: Reject requests without valid Bearer token prefix 92 auth_header = request.headers.get("authorization") 93 if not auth_header or not auth_header.startswith("Bearer "): 94 return Response( 95 '{"error": "Missing Bearer token"}', 96 status_code=401, 97 headers=WWW_HEADER, 98 media_type="application/json" 99 ) 100 101 token = auth_header.split("Bearer ", 1)[1].strip() 102 103 # Validate token using Scalekit SDK 104 # Security: Verifies signature, expiration, issuer, and audience claims 105 # Context: This critical step prevents accepting tokens from other issuers 106 options = TokenValidationOptions( 107 issuer=SK_ENV_URL, 108 audience=[EXPECTED_AUDIENCE] 109 ) 110 111 try: 112 is_valid = scalekit_client.validate_access_token(token, options=options) 113 if not is_valid: 114 raise ValueError("Invalid token") 115 except Exception: 116 return Response( 117 '{"error": "Token validation failed"}', 118 status_code=401, 119 headers=WWW_HEADER, 120 media_type="application/json" 121 ) 122 123 # Token is valid, proceed with request 124 # This allows MCP clients to call tools with authenticated context 125 return await call_next(request) 126 127 128 @app.get("/.well-known/oauth-protected-resource") 129 async def oauth_metadata(): 130 """ 131 Use case: Expose OAuth resource metadata for MCP client discovery 132 This endpoint allows clients to discover authorization requirements and server capabilities 133 Context: MCP clients use this metadata to initiate the OAuth flow 134 """ 135 if not PROTECTED_RESOURCE_METADATA: 136 return Response( 137 '{"error": "PROTECTED_RESOURCE_METADATA config missing"}', 138 status_code=500, 139 media_type="application/json" 140 ) 141 142 metadata = json.loads(PROTECTED_RESOURCE_METADATA) 143 return Response( 144 json.dumps(metadata, indent=2), 145 media_type="application/json" 146 ) 147 148 149 @app.get("/health") 150 async def health_check(): 151 """ 152 Use case: Health check endpoint for monitoring and load balancers 153 Context: Keep this separate from protected endpoints for deployment health checks 154 """ 155 return {"status": "healthy"} 156 157 158 # Mount the FastMCP application at root path 159 app.mount("/", mcp_app) 160 161 162 if __name__ == "__main__": 163 import uvicorn 164 # Start server with auto-reload for development 165 # Production: Use 'uvicorn main:app --host 0.0.0.0 --port 3002 --workers 4' behind a reverse proxy 166 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 1 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 1 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=69cce21a4f77360008b1503a) 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 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=69cce21a4f77360008b1503a) | 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 1 mkdir -p fastmcp-todo 2 cd fastmcp-todo 3 python3 -m venv venv 4 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 1 cat <<'EOF' > requirements.txt 2 fastmcp>=2.13.0.2 3 python-dotenv>=1.0.0 4 EOF 5 6 pip install -r requirements.txt 7 8 cat <<'EOF' > env.example 9 PORT=3002 10 SCALEKIT_ENVIRONMENT_URL=https://your-environment-url.scalekit.com 11 SCALEKIT_CLIENT_ID=your_client_id 12 SCALEKIT_RESOURCE_ID=mcp_server_id 13 MCP_URL=http://localhost:3002/ 14 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 1 cp env.example .env 2 open .env ``` | Variable | Description | | -------------------------- | ---------------------------------------------------------------------------------------- | | `SCALEKIT_ENVIRONMENT_URL` | Your Scalekit environment URL from **Dashboard > Settings** | | `SCALEKIT_CLIENT_ID` | Client ID from **Dashboard > Settings** | | `SCALEKIT_RESOURCE_ID` | The resource identifier assigned to your MCP server (starts with `res_`) | | `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 1 source venv/bin/activate 2 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=69cce21a4f77360008b1503a) 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 1 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=69cce21a4f77360008b1503a) 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 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. ## 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: ### 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=69cce21a4f77360008b1503a) --- # 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. ## 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. 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=69cce21a4f77360008b1503a) ### 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=69cce21a4f77360008b1503a) 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. ### 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. --- # 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 ### 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. #### 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. ### 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 ## 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 --- # 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) *** ## 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=69cce21a4f77360008b1503a) 2. Click **Create Client**. ![Create client placeholder](/.netlify/images?url=_astro%2Fmcp-clients-tab.UgPaVUGm.png\&w=3020\&h=1040\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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 1 curl --location '{{env_url}}/oauth/token' \ 2 --header 'Content-Type: application/x-www-form-urlencoded' \ 3 --data-urlencode 'grant_type=client_credentials' \ 4 --data-urlencode 'client_id={{client_id}}' \ 5 --data-urlencode 'client_secret={{secret_value}}' \ 6 --data-urlencode 'scope=todo:read todo:write' ``` Scalekit responds with a JSON payload similar to: ```json 1 { 2 "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIn0...", 3 "token_type": "Bearer", 4 "expires_in": 3600, 5 "scope": "todo:read todo:write" 6 } ``` 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) *** ## 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. ### 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. 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. ### 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 This guide helps you diagnose and resolve common issues when integrating Scalekit as an authentication server for your MCP servers. When you add authentication to MCP servers, you may encounter configuration problems, network issues, or client-specific limitations. This reference covers the most common scenarios and provides step-by-step solutions. Use this guide to troubleshoot setup problems, resolve CORS and network issues, understand client-specific behavior, and implement best practices for your authentication setup. ## Configuration & Setup Issues [Section titled “Configuration & Setup Issues”](#configuration--setup-issues) ### I’m getting an access token but no refresh token [Section titled “I’m getting an access token but no refresh token”](#im-getting-an-access-token-but-no-refresh-token) Add the `offline_access` scope to your authorization request. Without it, Scalekit does not issue a refresh token alongside the access token. Include it with your other scopes: ```plaintext 1 openid profile email offline_access ``` Once added, subsequent logins will return both an access token and a refresh token. *** ### My MCP server is not connecting to the MCP Inspector [Section titled “My MCP server is not connecting to the MCP Inspector”](#my-mcp-server-is-not-connecting-to-the-mcp-inspector) When your MCP server fails to connect to the MCP Inspector, this typically indicates a problem with the authentication handshake or metadata configuration. Follow these diagnosis steps to identify the issue. **Verify the MCP server is responding correctly:** 1. Open your browser’s developer tools (Network tab) 2. Navigate to your MCP server URL (e.g., `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, check the CORS & Network Issues section below. ### I’m getting a redirect\_uri mismatch error during authorization [Section titled “I’m getting a redirect\_uri mismatch error during authorization”](#im-getting-a-redirect_uri-mismatch-error-during-authorization) This error typically occurs when your MCP client has cached an old MCP server domain after you’ve changed it. The client continues sending requests to the old URL, which doesn’t match your current 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 environment switch [Section titled “GitHub Copilot CLI: stale cached credentials after environment switch”](#github-copilot-cli-stale-cached-credentials-after-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`. **Resolution:** 1. Locate and delete the cached OAuth config files: ```sh 1 rm -rf ~/.copilot/mcp-oauth-config ``` 2. Reconnect your MCP server in GitHub Copilot CLI — it will register a fresh client against the correct environment. Note If you cannot find the files using the path above, also check `~/.config/github-copilot/` for any cached MCP auth files. *** ## CORS & Network Issues [Section titled “CORS & Network Issues”](#cors--network-issues) ### I see CORS errors in the network logs when using MCP Inspector [Section titled “I see CORS errors in the network logs when using MCP Inspector”](#i-see-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. This prevents the authentication flow from completing successfully. **Resolution:** 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 Ensure you add callback URLs for both your development (`http://localhost:6274/`) and production environments to avoid CORS errors in either environment. ### Calls from the MCP client are not reaching my MCP server [Section titled “Calls from the MCP client are not reaching my MCP server”](#calls-from-the-mcp-client-are-not-reaching-my-mcp-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 using CDN services. **Troubleshooting steps:** 1. Check if you’re using a proxy (e.g., Cloudflare, AWS WAF, 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 proxy, if possible) Note Some corporate proxies require explicit whitelisting of authentication endpoints. Contact your network administrator if you suspect this is the case. *** ## Client-Specific Issues [Section titled “Client-Specific Issues”](#client-specific-issues) ### Claude Desktop ignores custom ports when connecting to MCP servers [Section titled “Claude Desktop ignores custom ports when connecting to MCP servers”](#claude-desktop-ignores-custom-ports-when-connecting-to-mcp-servers) Claude Desktop currently only supports standard HTTPS traffic on port `443`. If your MCP server runs on a custom port (e.g., `https://mymcp.internal:8443/`), Claude Desktop will still attempt to connect to port `443`, causing the connection to fail. **Workaround options:** 1. Expose your MCP server on port `443` (requires a proxy or load balancer) 2. 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 [Section titled “Multiple authentication tabs open when using both MCP-Remote and Claude Desktop”](#multiple-authentication-tabs-open-when-using-both-mcp-remote-and-claude-desktop) Recent versions of Claude Desktop have introduced Connectors functionality, eliminating the need to run MCP-Remote separately. Claude Desktop includes a **Custom Connector** feature that allows you to configure MCP servers directly without additional tools. **Recommendation:** * 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 requiring both, contact Claude’s official support Tip To avoid duplicate authentication flows, ensure you’re using only one MCP client at a time. ### My browser is not getting invoked during authentication [Section titled “My browser is not getting invoked during authentication”](#my-browser-is-not-getting-invoked-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 timeout, preventing successful authentication. **Resolution by operating system:** **macOS:** 1. Open **System Preferences > Security & Privacy > App Management** 2. Ensure 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. Ensure `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 updating permissions, always restart your MCP client to ensure 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 deploying 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 (e.g., `SCALEKIT_ENVIRONMENT_URL`, `MCP_SERVER_URL`) to easily switch between development and production environments. --- # 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=69cce21a4f77360008b1503a) 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. ### Build with a coding agent * Claude Code ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` ```bash /plugin install modular-sso@scalekit-auth-stack ``` * Codex ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` ```bash # Restart Codex # Plugin Directory -> Scalekit Auth Stack -> install modular-sso ``` * GitHub Copilot CLI ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` ```bash copilot plugin install modular-sso@scalekit-auth-stack ``` * 40+ agents ```bash npx skills add scalekit-inc/skills --skill modular-sso ``` [Continue building with AI →](/dev-kit/build-with-ai/sso/) 1. ## Configure “Modular Auth” mode [Section titled “Configure “Modular Auth” mode”](#configure-modular-auth-mode) Configure your environment to use Modular SSO. 1. Go to Dashboard > Authentication > General 2. Under “Full-Stack Auth” section, click “Disable Full-Stack Auth” 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` 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 ``` 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& # "offline_access" is required to receive a refresh token 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 1 // Validate and decode the ID token from the authentication result 2 const idTokenClaims = await scalekit.validateToken(result.idToken); 3 4 // Validate and decode the access token 5 const accessTokenClaims = await scalekit.validateToken(result.accessToken); ``` * Python Validate tokens ```py 1 # Validate and decode the ID token from the authentication result 2 id_token_claims = scalekit_client.validate_token(result["id_token"]) 3 4 # Validate and decode the access token 5 access_token_claims = scalekit_client.validate_token(result["access_token"]) ``` * Go Validate tokens ```go 1 // Validate and decode the access token (uses JWKS from the client) 2 accessTokenClaims, err := scalekitClient.GetAccessTokenClaims(ctx, result.AccessToken) 3 if err != nil { 4 // handle error 5 } ``` * Java Validate tokens ```java 1 // Validate and decode the ID token 2 Map idTokenClaims = scalekitClient.validateToken(result.getIdToken()); 3 4 // Validate and decode the access token 5 Map accessTokenClaims = scalekitClient.validateToken(result.getAccessToken()); ``` - Auth result ```js 1 { 2 user: { 3 email: 'john@example.com', 4 familyName: 'Doe', 5 givenName: 'John', 6 username: 'john@example.com', 7 id: 'conn_70087756662964366;dcc62570-6a5a-4819-b11b-d33d110c7716' 8 }, 9 idToken: 'eyJhbGciOiJSU..bcLQ', 10 accessToken: 'eyJhbGciO..', 11 expiresIn: 899 12 } ``` - 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 1 { 2 "iss": "", 3 "sub": "conn_70087756662964366;dcc62570-6a5a-4819-b11b-d33d110c7716", 4 "aud": [ 5 "skc_70087756327420046" 6 ], 7 "exp": 1758693916, 8 "iat": 1758693016, 9 "nbf": 1758693016, 10 "client_id": "skc_70087756327420046", 11 "jti": "tkn_91646913048216109" 12 } ``` 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=69cce21a4f77360008b1503a) 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](/guides/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.connections.listConnectionsByDomain({ 6 domain 7 }); 8 9 if (connections.length > 0) { 10 // Domain has SSO configured - redirect to identity provider 11 const authUrl = scalekit.getAuthorizationUrl(redirectUri, { 12 domainHint: domain // Automatically routes to correct IdP 13 }); 14 return res.redirect(authUrl); 15 } else { 16 // No SSO for this domain - show alternative login methods 17 return showPasswordlessLogin(); 18 } ``` * 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.connections.list_connections_by_domain( 6 domain=domain 7 ) 8 9 if len(connections) > 0: 10 # Domain has SSO configured - redirect to identity provider 11 options = AuthorizationUrlOptions() 12 options.domain_hint = domain # Automatically routes to correct IdP 13 14 auth_url = scalekit_client.get_authorization_url( 15 redirect_uri=redirect_uri, 16 options=options 17 ) 18 return redirect(auth_url) 19 else: 20 # No SSO for this domain - show alternative login methods 21 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 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. This guide shows you how to implement both integration methods. For the broader customer onboarding workflow, see [Onboard enterprise customers](/sso/guides/onboard-enterprise-customers/). ## Generate shareable portal link No-code 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. Navigate to **Dashboard > 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 Programmatic 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` ### 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 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 async function generatePortalLink(organizationId) { 10 const link = await scalekit.organization.generatePortalLink(organizationId); 11 return link.location; // Use as iframe src 12 } ``` * Python Flask ```python 6 collapsed lines 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 def generate_portal_link(organization_id): 11 link = scalekit_client.organization.generate_portal_link(organization_id) 12 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. ### 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. ## 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/). [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 --- # Code samples > Code samples demonstrating Single Sign-On implementations with Express.js, .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 .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 --- # 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` > Auth.js v5 defaults to `/auth` as its base path — not `/api/auth`. The callback URL must match exactly 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 1 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 1 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 1 # Generate with: npx auth secret 2 AUTH_SECRET= 3 4 # From Scalekit dashboard → API Keys 5 AUTH_SCALEKIT_ISSUER=https://yourenv.scalekit.dev 6 AUTH_SCALEKIT_ID=skc_... 7 AUTH_SCALEKIT_SECRET= 8 9 # Connection ID for development routing (conn_...) 10 # In production, resolve this dynamically per tenant — see step 7 11 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 1 import { handlers } from "@/auth" 2 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 1 Scalekit({ 2 issuer: process.env.AUTH_SCALEKIT_ISSUER!, 3 clientId: process.env.AUTH_SCALEKIT_ID!, 4 clientSecret: process.env.AUTH_SCALEKIT_SECRET!, 5 6 // Option A — exact connection (dev / single-tenant use) 7 connectionId: "conn_...", 8 9 // Option B — org's active connection (multi-tenant: look up org from user's DB record) 10 organizationId: "org_...", 11 12 // Option C — resolve org from email domain (useful at login prompt) 13 domain: "acme.com", 14 }) ``` 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 1 // Example: look up org at sign-in time 2 const org = await db.organizations.findByDomain(emailDomain) 3 4 await signIn("scalekit", { 5 organizationId: org.scalekitOrgId, 6 redirectTo: "/dashboard", 7 }) ``` ### 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 1 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`. Auth.js v5 changed the default `basePath` from `/api/auth` to `/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 --- # 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 1 // Use case: Get user's organizations for your switcher UI 2 // Security: Always validate session ownership before returning org data 3 const session = await scalekit.session.getSession(sessionId); 4 5 // Extract organizations from the session response 6 const organizations = session.authenticated_organizations || []; 7 8 // Render your organization switcher with this data 9 res.json({ organizations }); ``` * Python Flask ```python 1 # Use case: Get user's organizations for your switcher UI 2 # Security: Always validate session ownership before returning org data 3 session = scalekit_client.session.get_session(session_id) 4 5 # Extract organizations from the session response 6 organizations = session.get('authenticated_organizations', []) 7 8 # Render your organization switcher with this data 9 return jsonify({'organizations': organizations}) ``` * Go Gin ```go 1 // Use case: Get user's organizations for your switcher UI 2 // Security: Always validate session ownership before returning org data 3 session, err := scalekitClient.Session().GetSession(ctx, sessionId) 4 if err != nil { 5 return err 6 } 7 8 // Extract organizations from the session response 9 organizations := session.AuthenticatedOrganizations 10 11 // Render your organization switcher with this data 12 c.JSON(http.StatusOK, gin.H{"organizations": organizations}) ``` * Java Spring ```java 1 // Use case: Get user's organizations for your switcher UI 2 // Security: Always validate session ownership before returning org data 3 Session session = scalekitClient.sessions().getSession(sessionId); 4 5 // Extract organizations from the session response 6 List organizations = session.getAuthenticatedOrganizations(); 7 8 // Render your organization switcher with this data 9 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 1 // Example: Fetch domains for an organization 2 const domains = await scalekit.domains.list({ organizationId: 'org_123' }); 3 4 // 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.session.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 daily briefing agent with Vercel AI SDK and Scalekit Agent Auth > Connect a TypeScript or Python agent via Vercel AI SDK and Scalekit Agent Auth to Google Calendar and Gmail using two integration patterns. 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, handles token refresh automatically, and gives you a single API surface regardless of which provider you’re talking to. This recipe shows how to use it with Google Calendar and Gmail — and demonstrates two patterns for consuming those credentials in your agent. **What this recipe covers:** * **OAuth token pattern** — Scalekit provides a valid token; your agent calls the Google Calendar REST API directly. Use this when you need full control over the request. * **Built-in action pattern** — Your agent calls `execute_tool("gmail_fetch_mails")`; Scalekit executes the Gmail API call and returns structured data. Use this when you want speed and don’t need to customize the request. 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 **Agent Auth → Connections**: * `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 1 cd typescript 2 pnpm install ``` The `typescript/package.json` includes: ```json 1 { 2 "dependencies": { 3 "ai": "^4.3.15", 4 "@ai-sdk/anthropic": "^1.2.12", 5 "@scalekit-sdk/node": "2.2.0-beta.1", 6 "zod": "^3.0.0", 7 "dotenv": "^16.0.0" 8 } 9 } ``` * Python ```bash 1 cd python 2 uv venv .venv 3 uv pip install -r requirements.txt ``` The `python/requirements.txt` includes: ```text 1 scalekit-sdk-python 2 anthropic 3 requests 4 python-dotenv ``` ### 3. Configure credentials [Section titled “3. Configure credentials”](#3-configure-credentials) Copy the example env file and fill in your credentials: ```bash 1 cp typescript/.env.example typescript/.env # TypeScript 2 cp typescript/.env.example python/.env # Python (same variables) ``` .env ```bash 1 SCALEKIT_ENV_URL=https://your-env.scalekit.dev 2 SCALEKIT_CLIENT_ID=skc_... 3 SCALEKIT_CLIENT_SECRET=your-secret 4 5 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 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import { ConnectorStatus } from '@scalekit-sdk/node/lib/pkg/grpc/scalekit/v1/connected_accounts/connected_accounts_pb.js'; 3 import 'dotenv/config'; 4 5 // Never hard-code credentials — they would be exposed in source control. 6 // Pull them from environment variables at runtime. 7 const scalekit = new ScalekitClient( 8 process.env.SCALEKIT_ENV_URL!, 9 process.env.SCALEKIT_CLIENT_ID!, 10 process.env.SCALEKIT_CLIENT_SECRET!, 11 ); 12 13 const USER_ID = 'user_123'; // Replace with the real user ID from your session ``` `ConnectorStatus` is imported from the SDK’s generated protobuf file. 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 import requests 4 from datetime import datetime, timezone 5 from dotenv import load_dotenv 6 import anthropic 7 import scalekit.client 8 9 load_dotenv() 10 11 # Never hard-code credentials — they would be exposed in source control. 12 # Pull them from environment variables at runtime. 13 scalekit_client = scalekit.client.ScalekitClient( 14 client_id=os.environ["SCALEKIT_CLIENT_ID"], 15 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 16 env_url=os.environ["SCALEKIT_ENV_URL"], 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 all connected-account operations: creating accounts, generating auth links, fetching tokens, and executing built-in tools. ### 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(connector: string) { 2 const { connectedAccount } = 3 await scalekit.connectedAccounts.getOrCreateConnectedAccount({ 4 connector, 5 identifier: USER_ID, 6 }); 7 8 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 9 const { link } = 10 await scalekit.connectedAccounts.getMagicLinkForConnectedAccount({ 11 connector, 12 identifier: USER_ID, 13 }); 14 console.log(`\n[${connector}] Authorization required.`); 15 console.log(`Open this link:\n\n ${link}\n`); 16 console.log('Press Enter once you have completed the OAuth flow...'); 17 await new Promise(resolve => { 18 process.stdin.resume(); 19 process.stdin.once('data', () => { process.stdin.pause(); resolve(); }); 20 }); 21 } 22 23 return connectedAccount; 24 } ``` * Python ```python 1 def ensure_connected(connector: str): 2 response = actions.get_or_create_connected_account( 3 connection_name=connector, 4 identifier=USER_ID, 5 ) 6 connected_account = response.connected_account 7 8 if connected_account.status != "ACTIVE": 9 link_response = actions.get_authorization_link( 10 connection_name=connector, 11 identifier=USER_ID, 12 ) 13 print(f"\n[{connector}] Authorization required.") 14 print(f"Open this link:\n\n {link_response.link}\n") 15 input("Press Enter once you have completed the OAuth flow...") 16 17 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 using the OAuth token pattern [Section titled “6. Fetch calendar events using the OAuth token pattern”](#6-fetch-calendar-events-using-the-oauth-token-pattern) For Google Calendar, retrieve a valid access token from Scalekit and call the Google Calendar REST API directly. This pattern gives you full control over query parameters, pagination, and error handling. * TypeScript ```typescript 1 async function getAccessToken(connector: string): Promise { 2 const response = 3 await scalekit.connectedAccounts.getConnectedAccountByIdentifier({ 4 connector, 5 identifier: USER_ID, 6 }); 7 const details = response?.connectedAccount?.authorizationDetails?.details; 8 if (details?.case === 'oauthToken' && details.value?.accessToken) { 9 return details.value.accessToken; 10 } 11 throw new Error(`No access token found for ${connector}`); 12 } ``` Use this token in a tool that the LLM can call: ```typescript 1 import { tool } from 'ai'; 2 import { z } from 'zod'; 3 4 const today = new Date(); 5 const timeMin = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString(); 6 const timeMax = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59).toISOString(); 7 8 const calendarToken = await getAccessToken('googlecalendar'); 9 10 const getCalendarEvents = tool({ 11 description: "Fetch today's events from Google Calendar", 12 parameters: z.object({ 13 maxResults: z.number().optional().default(5), 14 }), 15 execute: async ({ maxResults }) => { 16 const url = new URL('https://www.googleapis.com/calendar/v3/calendars/primary/events'); 17 url.searchParams.set('timeMin', timeMin); 18 url.searchParams.set('timeMax', timeMax); 19 url.searchParams.set('maxResults', String(maxResults)); 20 url.searchParams.set('orderBy', 'startTime'); 21 url.searchParams.set('singleEvents', 'true'); 22 23 const res = await fetch(url.toString(), { 24 headers: { Authorization: `Bearer ${calendarToken}` }, 25 }); 26 if (!res.ok) throw new Error(`Calendar API error: ${res.status}`); 27 const data = await res.json() as { items?: unknown[] }; 28 return data.items ?? []; 29 }, 30 }); ``` * Python ```python 1 def get_access_token(connector: str) -> str: 2 # get_connected_account always returns a fresh token — 3 # Scalekit refreshes expired tokens before returning. 4 response = actions.get_connected_account( 5 connection_name=connector, 6 identifier=USER_ID, 7 ) 8 tokens = response.connected_account.authorization_details["oauth_token"] 9 return tokens["access_token"] 10 11 def fetch_calendar_events(access_token: str, max_results: int = 5) -> list: 12 today = datetime.now(timezone.utc).astimezone() 13 time_min = today.replace(hour=0, minute=0, second=0, microsecond=0).isoformat() 14 time_max = today.replace(hour=23, minute=59, second=59, microsecond=0).isoformat() 15 16 resp = requests.get( 17 "https://www.googleapis.com/calendar/v3/calendars/primary/events", 18 headers={"Authorization": f"Bearer {access_token}"}, 19 params={ 20 "timeMin": time_min, 21 "timeMax": time_max, 22 "maxResults": max_results, 23 "orderBy": "startTime", 24 "singleEvents": "true", 25 }, 26 ) 27 resp.raise_for_status() 28 return resp.json().get("items", []) ``` ### 7. Fetch emails using the built-in action pattern [Section titled “7. Fetch emails using the built-in action pattern”](#7-fetch-emails-using-the-built-in-action-pattern) For Gmail, call `execute_tool` with the built-in `gmail_fetch_mails` action. Scalekit executes the Gmail API call using the stored token and returns structured data. You don’t need to build the request, handle the token, or parse the response format. * 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 scalekit.tools.executeTool({ 8 toolName: 'gmail_fetch_mails', 9 connectedAccountId: gmailAccount?.id, 10 params: { 11 query: 'is:unread', 12 max_results: maxResults, 13 }, 14 }); 15 return response.data?.toJson() ?? {}; 16 }, 17 }); ``` * Python ```python 1 def fetch_unread_emails(connected_account_id: str, max_results: int = 5) -> dict: 2 response = actions.execute_tool( 3 tool_name="gmail_fetch_mails", 4 connected_account_id=connected_account_id, 5 tool_input={ 6 "query": "is:unread", 7 "max_results": max_results, 8 }, 9 ) 10 return response.result ``` The built-in action pattern trades flexibility for brevity. You can’t customize headers or pagination, but you also don’t need to read Gmail API documentation — the tool parameters are consistent across all Scalekit connectors. See [all supported agent connectors](/guides/integrations/agent-connectors/) for the full list of built-in tools. ### 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 const [calendarAccount, gmailAccount] = await Promise.all([ 5 ensureConnected('googlecalendar'), 6 ensureConnected('gmail'), 7 ]); 8 9 const calendarToken = await getAccessToken('googlecalendar'); 10 11 const { text } = await generateText({ 12 model: anthropic('claude-sonnet-4-6'), 13 prompt: `Give me a summary of my day for ${today.toDateString()}: list today's calendar events and my top 5 unread emails.`, 14 tools: { 15 getCalendarEvents, 16 getUnreadEmails, 17 }, 18 maxSteps: 5, // allow the LLM to call multiple tools before responding 19 }); 20 21 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 gmail_account = ensure_connected("gmail") 3 ensure_connected("googlecalendar") 4 calendar_token = get_access_token("googlecalendar") 5 6 client = anthropic.Anthropic() 7 today = datetime.now().strftime("%A, %B %d, %Y") 8 9 tools = [ 10 { 11 "name": "get_calendar_events", 12 "description": "Fetch today's events from Google Calendar", 13 "input_schema": { 14 "type": "object", 15 "properties": {"max_results": {"type": "integer", "default": 5}}, 16 }, 17 }, 18 { 19 "name": "get_unread_emails", 20 "description": "Fetch top unread emails from Gmail via Scalekit actions", 21 "input_schema": { 22 "type": "object", 23 "properties": {"max_results": {"type": "integer", "default": 5}}, 24 }, 25 }, 26 ] 27 28 messages = [ 29 { 30 "role": "user", 31 "content": f"Give me a summary of my day for {today}: list today's calendar events and my top 5 unread emails.", 32 } 33 ] 34 35 while True: 36 response = client.messages.create( 37 model="claude-sonnet-4-6", 38 max_tokens=1024, 39 tools=tools, 40 messages=messages, 41 ) 42 messages.append({"role": "assistant", "content": response.content}) 43 44 if response.stop_reason == "end_turn": 45 for block in response.content: 46 if hasattr(block, "text"): 47 print(block.text) 48 break 49 50 tool_results = [] 51 for block in response.content: 52 if block.type == "tool_use": 53 max_results = block.input.get("max_results", 5) 54 if block.name == "get_calendar_events": 55 result = fetch_calendar_events(calendar_token, max_results) 56 elif block.name == "get_unread_emails": 57 result = fetch_unread_emails(gmail_account.id, max_results) 58 else: 59 result = {"error": f"Unknown tool: {block.name}"} 60 tool_results.append({ 61 "type": "tool_result", 62 "tool_use_id": block.id, 63 "content": json.dumps(result), 64 }) 65 66 if tool_results: 67 messages.append({"role": "user", "content": tool_results}) 68 else: 69 break 70 71 if __name__ == "__main__": 72 run_agent() ``` ### 9. Testing [Section titled “9. Testing”](#9-testing) Run the agent: * TypeScript ```bash 1 cd typescript && pnpm start ``` * Python ```bash 1 cd python && .venv/bin/python index.py ``` On first run, you see two authorization prompts in sequence: ```text 1 [googlecalendar] Authorization required. 2 Open this link: 3 4 https://auth.scalekit.dev/connect/... 5 6 Press Enter once you have completed the OAuth flow... 7 8 [gmail] Authorization required. 9 Open this link: 10 11 https://auth.scalekit.dev/connect/... 12 13 Press Enter once you have completed the OAuth flow... ``` After both connectors are authorized, the agent fetches your data and returns a summary: ```text 1 Here's your day for Friday, March 27, 2026: 2 3 📅 Calendar — 3 events today 4 • 9:00 AM Team standup (30 min) 5 • 1:00 PM Product review 6 • 4:00 PM 1:1 with manager 7 8 📧 Unread emails — top 5 9 • "Q1 roadmap feedback needed" — Sarah Chen, 1h ago 10 • "Deploy failed: production" — GitHub Actions, 2h ago 11 • "New PR review requested" — Lin Feng, 3h ago 12 ... ``` 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 the SDK’s generated protobuf file and compare against `ConnectorStatus.ACTIVE` Python naive datetimes in API calls * **Symptom**: Google Calendar returns a `400` error for your event query * **Cause**: A naive `datetime` produces an ISO string without timezone information * **Fix**: Use `datetime.now(timezone.utc)` and call `.astimezone()` so the generated timestamps are timezone-aware `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 `toolInput` used instead of `params` * **Symptom**: `executeTool` succeeds but the Gmail tool receives no parameters * **Cause**: `@scalekit-sdk/node` expects a `params` field, not `toolInput` * **Fix**: Pass tool arguments in `params`, for example `{ query: 'is:unread', max_results: 5 }` ## 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 tokens. **Token freshness** — `getConnectedAccountByIdentifier` (TypeScript) and `get_connected_account` (Python) always return a fresh token — Scalekit refreshes it before returning if it has expired. You do not need to track expiry or call a refresh endpoint. **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** — In Python, `response.result` is a dictionary whose structure depends on the tool. In TypeScript, `response.data?.toJson()` converts the protobuf response to a plain object. Log the raw response on first use to understand the shape before passing it to the LLM. **Rate limits** — The Google Calendar API and Gmail API both have per-user daily quotas. If your agent runs frequently, add exponential backoff around the API calls and cache calendar events across requests where freshness allows. ## Next steps [Section titled “Next steps”](#next-steps) * **Add more connectors** — The same `ensureConnected` pattern works for any Scalekit-supported connector. Swap the connector name and replace the Google API calls with the target service’s API. See [all supported connectors](/guides/integrations/agent-connectors/). * **Use the built-in Calendar action** — Scalekit also provides a `googlecalendar_list_events` built-in action. If you don’t need custom query parameters, switch the Calendar tool to `execute_tool` and remove the `getAccessToken` call entirely. * **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 agent auth quickstart** — For a broader overview of the connected-accounts model and supported providers, see the [agent auth quickstart](/agent-auth/quickstart/). --- # DOCUMENT BOUNDARY --- # Implementing 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 1 npm install @scalekit-sdk/node jsonwebtoken ``` Create `.env.local`: ```bash 1 SCALEKIT_ENVIRONMENT_URL=env_xxxx 2 SCALEKIT_CLIENT_ID=skc_xxx 3 SCALEKIT_CLIENT_SECRET=your_secret 4 APP_URL=http://localhost:3000 5 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 1 function getErrorMessage(error: string): string { 2 if (error.includes('expired')) { 3 return 'This link has expired. Request a new one.'; 4 } 5 if (error.includes('rate')) { 6 return 'Too many attempts. Please try again later.'; 7 } 8 if (error.includes('invalid')) { 9 return 'Invalid code. Please check and try again.'; 10 } 11 return 'Verification failed. Please try again.'; 12 } ``` ## 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 1 spring: 2 security: 3 oauth2: 4 resourceserver: 5 jwt: 6 issuer-uri: https://auth.scalekit.com ``` But authentication fails with timeout errors like: ```plaintext 1 java.net.SocketTimeoutException: Connect timed out 2 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) Configure explicit timeout values for the OAuth2 resource server’s HTTP client. Spring Security provides configuration properties specifically for this: * application.yml ```yaml 1 spring: 2 security: 3 oauth2: 4 resourceserver: 5 jwt: 6 issuer-uri: https://auth.scalekit.com 7 # Configure timeouts for JWKS and discovery endpoints 8 client: 9 registration: 10 connect-timeout: 10000 # 10 seconds for connection 11 read-timeout: 10000 # 10 seconds for reading response ``` * application.properties ```properties 1 spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth.scalekit.com 2 # Configure timeouts for JWKS and discovery endpoints 3 spring.security.oauth2.resourceserver.jwt.client.registration.connect-timeout=10000 4 spring.security.oauth2.resourceserver.jwt.client.registration.read-timeout=10000 ``` ### Timeout values explained [Section titled “Timeout values explained”](#timeout-values-explained) * **connect-timeout**: Maximum time (in milliseconds) to establish a connection to Scalekit’s servers * **read-timeout**: Maximum time (in milliseconds) to wait for a response after connection is established **Recommended values**: * **Development**: 5000ms (5 seconds) for faster feedback * **Production**: 10000-15000ms (10-15 seconds) to handle network variability ## Alternative: Programmatic configuration [Section titled “Alternative: Programmatic configuration”](#alternative-programmatic-configuration) If you need more control or want to configure this per-environment, use Java configuration: ```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 approach gives you: * Full control over HTTP client configuration * Ability to add custom headers or interceptors * Environment-specific timeout tuning ## 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 1 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 Java SDK Documentation](https://docs.scalekit.com/apis/#tag/authentication) * [Spring Boot 4.0 Release Notes](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Release-Notes) --- # 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. If you’re using a service account rather than user-delegated OAuth, or building in JavaScript, the pattern is the same but the source differs — see the `javascript/` track in [agent-auth-examples](https://github.com/scalekit-developers/agent-auth-examples). The complete Python source used here is `python/meeting_scheduler_agent.py` in that repo. **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 either returns an existing active account with a valid token or creates a new one and returns an authorization URL. Once the user authorizes, `get_connected_account` returns the token. From that point, Scalekit handles refresh automatically. This means your agent’s authorization step is a single function regardless of which connector you’re targeting. The rest of the code — Calendar queries, event creation, Gmail drafts — is plain HTTP with the token Scalekit provides. 1. **Set up the environment** Create a `.env` file at the project root with your Scalekit credentials: ```bash 1 SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com 2 SCALEKIT_CLIENT_ID=your-client-id 3 SCALEKIT_CLIENT_SECRET=your-client-secret ``` Install dependencies: ```bash 1 pip install scalekit-sdk python-dotenv requests ``` 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 import base64 3 from datetime import datetime, timezone, timedelta 4 from email.mime.text import MIMEText 5 6 import requests 7 from dotenv import load_dotenv 8 from scalekit import ScalekitClient 9 10 load_dotenv() 11 12 # Never hard-code credentials — they would be exposed in source control 13 # and CI logs. Pull them from environment variables instead. 14 scalekit_client = ScalekitClient( 15 environment_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), 16 client_id=os.getenv("SCALEKIT_CLIENT_ID"), 17 client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), 18 ) 19 20 actions = scalekit_client.actions 21 22 # Replace with a real user identifier from your application's session 23 USER_ID = "user_123" 24 ATTENDEE_EMAIL = "attendee@example.com" 25 MEETING_TITLE = "Quick Sync" 26 DURATION_MINUTES = 60 27 SEARCH_DAYS = 3 28 WORK_START_HOUR = 9 # UTC 29 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 handles the first-run prompt and returns a valid access token: ```python 1 def authorize(connector: str) -> str: 2 """Ensure the user has an active connected account and return its access token. 3 4 On first run, this prints an authorization URL and waits for the user 5 to complete the browser OAuth flow before continuing. 6 """ 7 account = actions.get_or_create_connected_account(connector, USER_ID) 8 9 if account.status != "active": 10 auth_link = actions.get_authorization_link(connector, USER_ID) 11 print(f"\nOpen this link to authorize {connector}:\n{auth_link}\n") 12 input("Press Enter after completing authorization in your browser…") 13 account = actions.get_connected_account(connector, USER_ID) 14 15 return account.authorization_details["oauth_token"]["access_token"] ``` Call this once per connector before any API calls: ```python 1 calendar_token = authorize("googlecalendar") 2 gmail_token = authorize("gmail") ``` After the first successful authorization, `get_or_create_connected_account` returns `status == "active"` on subsequent runs and the `if` block is skipped. Scalekit refreshes expired tokens automatically. 4. **Query calendar availability** With a valid Calendar token, query the `freeBusy` endpoint to get the user’s busy intervals: ```python 1 def get_busy_slots(token: str) -> 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 response = requests.post( 7 "https://www.googleapis.com/calendar/v3/freeBusy", 8 headers={"Authorization": f"Bearer {token}"}, 9 json={ 10 "timeMin": now.isoformat(), 11 "timeMax": window_end.isoformat(), 12 "items": [{"id": "primary"}], 13 }, 14 ) 15 response.raise_for_status() 16 return response.json()["calendars"]["primary"]["busy"] ``` `raise_for_status()` converts 4xx and 5xx responses into exceptions, so the caller sees a clear error rather than a silent wrong result. The `busy` list contains `{"start": "...", "end": "..."}` dicts in ISO 8601 format. 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 find_free_slot(busy_slots: list[dict]) -> tuple[datetime, datetime] | None: 2 """Return the first open one-hour slot during working hours in UTC. 3 4 Returns None if no slot is available in the search window. 5 """ 6 now = datetime.now(timezone.utc) 7 # Round up to the next whole hour so the candidate is always in the future 8 candidate = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) 9 window_end = now + timedelta(days=SEARCH_DAYS) 10 11 while candidate < window_end: 12 slot_end = candidate + timedelta(minutes=DURATION_MINUTES) 13 14 if WORK_START_HOUR <= candidate.hour < WORK_END_HOUR: 15 overlap = any( 16 candidate < datetime.fromisoformat(b["end"]) 17 and slot_end > datetime.fromisoformat(b["start"]) 18 for b in busy_slots 19 ) 20 if not overlap: 21 return candidate, slot_end 22 23 candidate += timedelta(hours=1) 24 25 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** Post the event to the Google Calendar API and return its HTML link, which you’ll include in the email draft: ```python 1 def create_event(token: str, start: datetime, end: datetime) -> str: 2 """Create a calendar event and return its HTML link.""" 3 response = requests.post( 4 "https://www.googleapis.com/calendar/v3/calendars/primary/events", 5 headers={"Authorization": f"Bearer {token}"}, 6 json={ 7 "summary": MEETING_TITLE, 8 "description": "Scheduled by agent", 9 "start": {"dateTime": start.isoformat(), "timeZone": "UTC"}, 10 "end": {"dateTime": end.isoformat(), "timeZone": "UTC"}, 11 "attendees": [{"email": ATTENDEE_EMAIL}], 12 }, 13 ) 14 response.raise_for_status() 15 return response.json()["htmlLink"] ``` The `htmlLink` in the response 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** Build the email body, base64-encode it, and post it to Gmail’s drafts endpoint: ```python 1 def create_draft(token: str, 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 message = MIMEText(body) 12 message["to"] = ATTENDEE_EMAIL 13 message["subject"] = f"Invitation: {MEETING_TITLE}" 14 15 # Gmail's API requires the raw RFC 2822 message encoded as URL-safe base64 16 raw = base64.urlsafe_b64encode(message.as_bytes()).decode() 17 18 response = requests.post( 19 "https://gmail.googleapis.com/gmail/v1/users/me/drafts", 20 headers={"Authorization": f"Bearer {token}"}, 21 json={"message": {"raw": raw}}, 22 ) 23 response.raise_for_status() 24 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 calendar_token = authorize("googlecalendar") 4 5 print("Authorizing Gmail…") 6 gmail_token = authorize("gmail") 7 8 print("Checking calendar availability…") 9 busy_slots = get_busy_slots(calendar_token) 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(calendar_token, start, end) 21 print(f"Event created: {event_link}") 22 23 print("Creating Gmail draft…") 24 create_draft(gmail_token, event_link, start) 25 26 27 if __name__ == "__main__": 28 main() ``` ## Testing [Section titled “Testing”](#testing) Run the agent from the command line: ```bash 1 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 string you pass to `authorize()` exactly. * **Missing OAuth scopes** — If you see a `403 Forbidden` when calling the Calendar or Gmail API, the OAuth app in Google Cloud Console is missing the required scopes. Calendar needs `https://www.googleapis.com/auth/calendar` and Gmail needs `https://www.googleapis.com/auth/gmail.compose`. * **`raise_for_status()` swallowing context** — The default exception message from `requests` truncates the response body. In development, add `print(response.text)` before `raise_for_status()` to see the full error from Google. * **UTC times without timezone info** — Passing a naive `datetime` (without `timezone.utc`) to `isoformat()` produces a string without a `Z` suffix. Google Calendar rejects this with a `400` error. 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 connected account query returns the wrong user’s tokens. ## 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 `freeBusy` query checks only `primary`. Users who manage work and personal calendars separately will show false availability. Expand the `items` list to include all calendars the user has shared access to. **Draft vs send** — Creating a draft is safer for a first deployment. When you’re confident in the agent’s output quality, switch the Gmail endpoint from `/drafts` to `/messages/send` 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: track the event ID and delete it 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 `requests.post` 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 `agent-auth-examples` repo includes a JavaScript track. Compare the two implementations to see where the patterns converge and where they differ. * **Handle re-authorization** — If a user revokes access, `get_connected_account` returns an inactive account. Add a re-authorization path to recover gracefully instead of crashing. * **Explore other connectors** — The same `authorize()` pattern works for any Scalekit-supported connector: Slack, Notion, Jira. Swap the connector name and replace the Google API calls with the target service’s API. * **Review the Scalekit agent auth quickstart** — For a broader overview of the connected-accounts model, see the [agent auth quickstart](/agent-auth/quickstart). --- # 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 1 claude mcp add --transport http ref-context https://api.ref.tools/mcp \ 2 --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 1 { 2 "ref-context": { 3 "type": "http", 4 "url": "https://api.ref.tools/mcp?apiKey=YOUR_API_KEY" 5 } 6 } ``` #### Windsurf [Section titled “Windsurf”](#windsurf) Add the following to `~/.codeium/windsurf/mcp_config.json`: \~/.codeium/windsurf/mcp\_config.json ```json 1 { 2 "ref-context": { 3 "serverUrl": "https://api.ref.tools/mcp?apiKey=YOUR_API_KEY" 4 } 5 } ``` #### Other (stdio) [Section titled “Other (stdio)”](#other-stdio) For any MCP client that supports stdio, add to your MCP config: mcp.json ```json 1 { 2 "ref-context": { 3 "command": "npx", 4 "args": ["ref-tools-mcp@latest"], 5 "env": { 6 "REF_API_KEY": "YOUR_API_KEY" 7 } 8 } 9 } ``` 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 1 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 --- # Developer resources > Get up and running with SDKs, APIs, and integration tools Coming soon --- # DOCUMENT BOUNDARY --- # Claude Integration > Integrate Scalekit with Claude for AI-powered authentication workflows Coming soon --- # DOCUMENT BOUNDARY --- # Codex Integration > Use Scalekit with Codex for automated authentication code generation 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. Scalekit Auth Stack is under review on Cursor Marketplace The Scalekit Auth Stack plugin is currently under review and not yet live on [cursor.com/marketplace](https://cursor.com/marketplace). Once approved, you’ll be able to install it directly with an “Add to Cursor” button. Until then, use the local installer to load the plugins into Cursor. 1. ## Install the Scalekit Auth Stack locally Terminal ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/cursor-authstack/main/install.sh | bash ``` This installer downloads the latest Scalekit Cursor plugin bundle and installs each auth plugin into `~/.cursor/plugins/local/`. Use a symlink when iterating locally If you’re developing the plugin repo locally and want changes to show up without recopies, use the local installer path described in the repository README to symlink plugins into `~/.cursor/plugins/local`. 2. ## Reload Cursor and enable the plugin Restart Cursor, or run **Developer: Reload Window**, then open **Settings > Cursor Settings > Plugins**. Select the authentication plugin you need, such as **Full Stack Auth**, **Modular SSO**, or **MCP Auth**, and enable it. Alternatively: Install via Skills CLI You can also install Scalekit skills with the Vercel Skills CLI: Terminal ```bash npx skills add scalekit-inc/skills ``` Use `--list` to browse available skills or `--skill ` to install a specific auth type. Refer to Cursor’s documentation for how to invoke skills once installed. 3. ## Generate the implementation Open Cursor’s chat panel with **Cmd+L** (macOS) or **Ctrl+L** (Windows/Linux) and paste in an implementation prompt. Use the same prompt from the corresponding Claude Code tab — the Scalekit plugins and their authentication skills work identically in Cursor. 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 Once the Scalekit Auth Stack is live on [cursor.com/marketplace](https://cursor.com/marketplace), you’ll be able to skip the local installer and install it directly inside Cursor. --- # 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) The Scalekit MCP server can be configured to support OAuth for compatible clients. If your MCP Client doesn’t support OAuth based authorization for MCP Servers, you can still use the Scalekit MCP server with the mcp-remote acting as a local proxy to add OAuth support. ### Using OAuth (VS Code version 1.101 or greater) [Section titled “Using OAuth (VS Code version 1.101 or greater)”](#using-oauth-vs-code-version-1101-or-greater) ```json 1 { 2 "servers": { 3 "scalekit": { 4 "type": "http", 5 "url": "https://mcp.scalekit.com/" 6 } 7 } 8 } ``` ### Using mcp-remote proxy [Section titled “Using mcp-remote proxy”](#using-mcp-remote-proxy) ```json 1 { 2 "mcpServers": { 3 "scalekit": { 4 "command": "npx", 5 "args": ["-y", "mcp-remote", "https://mcp.scalekit.com/"] 6 } 7 } 8 } ``` Based on your MCP Host, configuration instructions to add Scalekit as an MCP Server can be found below: ### Claude Desktop [Section titled “Claude Desktop”](#claude-desktop) Configure the Claude app to use the MCP server: 1. Open the Claude Desktop app, go to Settings, then Developer 2. Click Edit Config 3. Open the claude\_desktop\_config.json file 4. Copy and paste the server config to your existing file, then save 5. Restart Claude ```json 1 { 2 "mcpServers": { 3 "scalekit": { 4 "command": "npx", 5 "args": ["-y", "mcp-remote", "https://mcp.scalekit.com/"] 6 } 7 } 8 } ``` ### Cursor [Section titled “Cursor”](#cursor) Configure Cursor to use the MCP server: 1. Open Cursor, go to Settings, then Cursor Settings 2. Select MCP on the left 3. Click Add “New Global MCP Server” at the top right 4. Copy and paste the server config to your existing file, then save 5. Restart Cursor ```json 1 { 2 "mcpServers": { 3 "scalekit": { 4 "command": "npx", 5 "args": ["-y", "mcp-remote", "https://mcp.scalekit.com/"] 6 } 7 } 8 } ``` ### Windsurf [Section titled “Windsurf”](#windsurf) Configure Windsurf to use the MCP server: 1. Open Windsurf, go to Settings, then Developer 2. Click Edit Config 3. Open the windsurf\_config.json file 4. Copy and paste the server config to your existing file, then save 5. Restart Windsurf ```json 1 { 2 "mcpServers": { 3 "scalekit": { 4 "command": "npx", 5 "args": ["-y", "mcp-remote", "https://mcp.scalekit.com/"] 6 } 7 } 8 } ``` ## Authentication for MCP Server [Section titled “Authentication for MCP Server”](#authentication-for-mcp-server) Scalekit MCP server uses OAuth2.1 based authentication. As soon as you register Scalekit MCP Server in your MCP Host, your MCP Host will initiate an OAuth authorization workflow so that the MCP Client can get appropriate tokens to securely communicate with Scalekit’s MCP Server. Tip If you are building your own MCP Server and would like to add OAuth based authorization, you can refer to our solution [Auth for MCP Servers](https://docs.scalekit.com/authenticate/mcp/quickstart). ## Github [Section titled “Github”](#github) We have made the source code for the Scalekit MCP server available on [Github](https://github.com/scalekit-inc/mcp). You can also find all available tools and descriptions in our Github repo. * Feel free to go through the code and raise an issue if you find any bugs or have any questions. * If you have suggestions for new tools, please raise a PR or open an issue. --- # DOCUMENT BOUNDARY --- # VS Code Extension > Enhance your development workflow with the Scalekit VS Code extension Coming soon --- # 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 --- # APIs > Learn how to work with Scalekit REST APIs, including authentication, pagination, error handling, and rate limits. The Scalekit REST APIs provide endpoints for authentication, user management, organization handling, and more. For the complete API reference, see the [REST API documentation](/apis/#description/overview). ## Authentication [Section titled “Authentication”](#authentication) Coming soon: API key authentication and examples. ## Pagination [Section titled “Pagination”](#pagination) Coming soon: Pagination patterns and examples. ## Error handling [Section titled “Error handling”](#error-handling) Coming soon: Status codes, error response format, and handling examples. ## External ID [Section titled “External ID”](#external-id) Coming soon: Using external IDs to correlate resources. ## Metadata [Section titled “Metadata”](#metadata) Coming soon: Storing custom key-value pairs on resources. ## Rate limits [Section titled “Rate limits”](#rate-limits) Coming soon: Rate limit details and retry patterns. --- # DOCUMENT BOUNDARY --- # Build with AI > Use AI coding agents to implement Scalekit authentication in minutes Pick the auth feature you need below. Each page gives you a ready-to-paste prompt for your coding agent — Claude Code, Cursor, GitHub Copilot CLI, or OpenCode. The agent reads your codebase, applies consistent patterns, and generates production-ready auth code in minutes. * Claude Code Step 1 — Add the marketplace (Claude REPL) ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` Step 2 — Install your auth plugin (Claude REPL) ```bash # options: full-stack-auth, agent-auth, mcp-auth, modular-sso, modular-scim /plugin install agent-auth@scalekit-auth-stack ``` Now ask your agent to implement Scalekit auth in natural language. * Codex Step 1 — Install the Scalekit Auth Stack ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` Step 2 — Restart Codex, open **Plugin Directory**, select **Scalekit Auth Stack**, and enable your auth plugin. Now ask your agent to implement Scalekit auth in natural language. * GitHub Copilot CLI Step 1 — Add the marketplace ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` Step 2 — Install your auth plugin ```bash # options: full-stack-auth, agent-auth, mcp-auth, modular-sso, modular-scim copilot plugin install agent-auth@scalekit-auth-stack ``` Now ask your agent to implement Scalekit auth in natural language. * Cursor The Scalekit Auth Stack is pending Cursor Marketplace review. Install it locally in Cursor: Step 1 — Install the Scalekit Auth Stack ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/cursor-authstack/main/install.sh | bash ``` Step 2 — Restart Cursor, open **Settings > Cursor Settings > Plugins**, and enable your auth plugin. Now ask your agent to implement Scalekit auth in natural language. * 40+ agents Works with OpenCode, Windsurf, Cline, Gemini CLI, Codex, and 35+ more agents via the [Vercel Skills CLI](https://vercel.com/docs/agent-resources/skills). Step 1 — Browse available skills ```bash npx skills add scalekit-inc/skills --list ``` Step 2 — Install a specific skill ```bash npx skills add scalekit-inc/skills --skill adding-mcp-oauth ``` Now ask your agent to implement Scalekit auth in natural language. ### [Full Stack Auth](/dev-kit/build-with-ai/full-stack-auth/) ### [Agent Auth](/dev-kit/build-with-ai/agent-auth/) ### [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 auth to your AI agents > Let your coding agents guide you into adding auth to your agent to handle OAuth tokens and call tools on behalf of the user Use AI coding agents like Claude Code, GitHub Copilot CLI, Cursor, and OpenCode to add Scalekit’s Agent Auth to your AI applications. This guide shows you how to configure these agents so they analyze your codebase, apply authentication patterns, and generate production-ready code for handling OAuth tokens and connecting to external services such as Gmail, Calendar, Slack, and Notion, reducing implementation time from hours to minutes while following security best practices. * Claude Code 1. ## Add the Scalekit Auth Stack marketplace Not yet on Claude Code? Follow the [official quickstart guide](https://code.claude.com/docs/en/quickstart) to install it. Register Scalekit’s plugin marketplace to access pre-configured authentication skills. This marketplace provides context-aware prompts and implementation guides that help coding agents generate correct Agent Auth code. Start the Claude Code REPL: Terminal ```bash claude ``` Then add the marketplace: Claude REPL ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` When the marketplace registers successfully, you’ll see confirmation output: Terminal ```bash ❯ /plugin marketplace add scalekit-inc/claude-code-authstack ⎿ Successfully added marketplace: scalekit-auth-stack ``` The marketplace provides specialized authentication plugins that understand Agent Auth patterns and OAuth 2.0 security requirements. These plugins guide the coding agent to generate implementation code that matches your project structure. 2. ## Enable authentication plugins Select which authentication capabilities to activate in your development environment. Each plugin provides specific skills that the coding agent uses to generate authentication code. Directly install the specific plugin: Claude REPL ```bash /plugin install agent-auth@scalekit-auth-stack ``` 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 Agent Auth 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. 3. ## Generate authentication implementation Use a structured prompt to direct the coding agent. A well-formed prompt ensures the agent generates complete, production-ready Agent Auth code that includes all required security components. Copy the following prompt into your coding agent: Authentication implementation prompt ```md Guide me through configuring the installed Scalekit marketplace plugin to handle agent authentication for Gmail. Provide the code to trigger the auth flow, retrieve the secure user token, and then use that authenticated session to fetch and list the last 5 unread emails. Add logging to verify the flow. ``` When you submit this prompt, Claude Code loads the Agent Auth skill from the marketplace -> analyzes your existing application structure -> generates Scalekit client initialization -> creates connected account management functions -> implements OAuth authorization link generation -> adds token fetching and refresh logic. 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 just-in-time implementation After the coding agent completes, verify that all authentication components are properly configured: Check generated files: * Scalekit client initialization with credentials. You may need to set up a `.env` file with your Scalekit API credentials. * Connected account management functions * Authorization link generation * Token fetching and storage * Error handling for expired tokens The authorization flow should redirect users to the service’s consent page, where they grant permissions. Your application should then be able to fetch OAuth tokens and execute actions on behalf of the authenticated user. When you connect, the agent authenticates users through the OAuth 2.0 flow you configured. Verify that protected resources require valid access tokens and that the agent can successfully execute actions on behalf of authenticated users. * Codex 1. ## Install the Scalekit Auth Stack marketplace Install Scalekit’s Codex-native marketplace to access focused authentication plugins and reusable implementation guidance. Run the bootstrap installer: Terminal ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` This installer downloads the marketplace from GitHub, installs it into `~/.codex/marketplaces/scalekit-auth-stack`, and only updates `~/.agents/plugins/marketplace.json` when it is safe to do so. If Codex skips your personal marketplace file The installer avoids overwriting another personal marketplace by default. If it skips that file, follow the installer’s manual path and select the marketplace from `~/.codex/marketplaces/scalekit-auth-stack/.agents/plugins/marketplace.json`. 2. ## Enable the Agent Auth plugin Restart Codex so it reloads installed marketplaces, then open the Plugin Directory and select **Scalekit Auth Stack**. Install the `agent-auth` plugin. This plugin includes the workflows, connector guidance, and references Codex uses to generate Agent Auth code for connected accounts and delegated OAuth flows. 3. ## Generate the authentication implementation Use a structured prompt to direct Codex. A well-formed prompt helps Codex generate complete, production-ready Agent Auth code that includes all required security components. Copy the following prompt into Codex: Authentication implementation prompt ```md Guide me through configuring the installed Scalekit marketplace plugin to handle agent authentication for Gmail. Provide the code to trigger the auth flow, retrieve the secure user token, and then use that authenticated session to fetch and list the last 5 unread emails. Add logging to verify the flow. ``` When you submit this prompt, Codex loads the Agent Auth plugin from the Scalekit Auth Stack marketplace, analyzes your existing application structure, generates Scalekit client initialization, creates connected account management functions, implements OAuth authorization link generation, and adds token fetching and refresh logic. 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 just-in-time implementation After Codex completes, verify that all authentication components are properly configured: Check generated files: * Scalekit client initialization with credentials. You may need to set up a `.env` file with your Scalekit API credentials. * Connected account management functions * Authorization link generation * Token fetching and storage * Error handling for expired tokens The authorization flow should redirect users to the service’s consent page, where they grant permissions. Your application should then be able to fetch OAuth tokens and execute actions on behalf of the authenticated user. When you connect, the agent authenticates users through the OAuth 2.0 flow you configured. Verify that protected resources require valid access tokens and that the agent can successfully execute actions on behalf of authenticated users. * GitHub Copilot CLI 1. ## Add the Scalekit authstack marketplace 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. Register Scalekit’s plugin marketplace to access pre-configured authentication plugins. This marketplace provides implementation skills that help GitHub Copilot generate correct Agent Auth code. Terminal ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` The marketplace provides specialized plugins that understand Agent Auth patterns and OAuth 2.0 security requirements. These plugins guide GitHub Copilot to generate implementation code that matches your project structure. 2. ## Install the Agent Auth plugin Install the Agent Auth plugin to give GitHub Copilot the skills needed to generate agent authentication code: Terminal ```bash copilot plugin install agent-auth@scalekit-auth-stack ``` Verify the plugin is installed Confirm the plugin installed successfully: Terminal ```bash copilot plugin list ``` Keep plugins updated Update authentication plugins regularly to receive security patches and improvements: Terminal ```bash copilot plugin update agent-auth@scalekit-auth-stack ``` 3. ## Generate authentication implementation Use a structured prompt to direct GitHub Copilot. A well-formed prompt ensures the agent generates complete, production-ready Agent Auth code that includes all required security components. Copy the following command into your terminal: Terminal ```bash copilot "Configure Scalekit agent authentication for Gmail — provide the code to trigger the auth flow, retrieve the secure user token, and then use that authenticated session to fetch and list the last 5 unread emails. Add logging to verify the flow." ``` GitHub Copilot uses the Agent Auth plugin to analyze your existing application structure, generate Scalekit client initialization code, create connected account management functions, implement OAuth authorization link generation, and add token fetching and refresh logic. 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 GitHub Copilot completes, verify that all authentication components are properly configured: Check generated files: * Scalekit client initialization with credentials (you may need to set up a `.env` file with your Scalekit API credentials) * Connected account management functions * Authorization link generation * Token fetching and storage * Error handling for expired tokens The authorization flow should redirect users to the service’s consent page, where they grant permissions. Your application should then be able to fetch OAuth tokens and execute actions on behalf of the authenticated user. When you connect, the agent authenticates users through the OAuth 2.0 flow you configured. Verify that protected resources require valid access tokens and that the agent can successfully execute actions on behalf of authenticated users. * Cursor Scalekit Auth Stack is under review on Cursor Marketplace The Scalekit Auth Stack plugin is currently under review and not yet live on [cursor.com/marketplace](https://cursor.com/marketplace). Once approved, you’ll be able to install it directly with an “Add to Cursor” button. Until then, use the local installer to load the plugins into Cursor. 1. ## Install the Scalekit Auth Stack locally Terminal ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/cursor-authstack/main/install.sh | bash ``` This installer downloads the latest Scalekit Cursor plugin bundle and installs each auth plugin into `~/.cursor/plugins/local/`. Use a symlink when iterating locally If you’re developing the plugin repo locally and want changes to show up without recopies, use the local installer path described in the repository README to symlink plugins into `~/.cursor/plugins/local`. 2. ## Reload Cursor and enable the plugin Restart Cursor, or run **Developer: Reload Window**, then open **Settings > Cursor Settings > Plugins**. Select the authentication plugin you need, such as **Full Stack Auth**, **Modular SSO**, or **MCP Auth**, and enable it. Alternatively: Install via Skills CLI You can also install Scalekit skills with the Vercel Skills CLI: Terminal ```bash npx skills add scalekit-inc/skills ``` Use `--list` to browse available skills or `--skill ` to install a specific auth type. Refer to Cursor’s documentation for how to invoke skills once installed. 3. ## Generate the implementation Open Cursor’s chat panel with **Cmd+L** (macOS) or **Ctrl+L** (Windows/Linux) and paste in an implementation prompt. Use the same prompt from the corresponding Claude Code tab — the Scalekit plugins and their authentication skills work identically in Cursor. 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 Once the Scalekit Auth Stack is live on [cursor.com/marketplace](https://cursor.com/marketplace), you’ll be able to skip the local installer and install it directly inside Cursor. * 40+ agents Scalekit skills work with 40+ AI agents via the [Vercel Skills CLI](https://vercel.com/docs/agent-resources/skills). Install skills to add Scalekit authentication to your agent. Supported agents include Claude Code, Cursor, GitHub Copilot CLI, OpenCode, Windsurf, Cline, Gemini CLI, Codex, and 30+ others. 1. ## Install interactively Run the command with no flags to be guided through the available skills: Terminal ```bash npx skills add scalekit-inc/skills ``` 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/skills --list # Install a specific skill npx skills add scalekit-inc/skills --skill adding-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/skills --all --global ``` This installs skills for Full Stack Auth, Agent Auth, MCP Auth, Modular SSO, and Modular SCIM. Explore available connectors Browse [Scalekit’s connector library](https://app.scalekit.com) > Agent Auth to see all available services you can integrate with your AI agents. New connectors are added regularly. --- # 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 Use AI coding agents like Claude Code, GitHub Copilot CLI, Cursor, and OpenCode to implement Scalekit’s full-stack authentication end-to-end in your web applications. This guide shows you how to configure these agents so they analyze your codebase, apply consistent authentication patterns, and generate production-ready code for login, session management, and logout that follows security best practices while reducing implementation time from hours to minutes. * Claude Code 1. ## Add the Scalekit Auth Stack marketplace Not yet on Claude Code? Follow the [official quickstart guide](https://code.claude.com/docs/en/quickstart) to install it. Register Scalekit’s plugin marketplace to access pre-configured authentication skills. This marketplace provides context-aware prompts and implementation guides that help coding agents generate correct Full Stack Auth code. Start the Claude Code REPL: Terminal ```bash claude ``` Then add the marketplace: Claude REPL ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` When the marketplace registers successfully, you’ll see confirmation output: Terminal ```bash ❯ /plugin marketplace add scalekit-inc/claude-code-authstack ⎿ Successfully added marketplace: scalekit-auth-stack ``` The marketplace provides specialized authentication plugins that understand full-stack auth patterns and OAuth 2.0 security requirements. These plugins guide the coding agent to generate implementation code that matches your project structure. 2. ## Enable authentication plugins Select which authentication capabilities to activate in your development environment. Each plugin provides specific skills that the coding agent uses to generate authentication code. Directly install the specific plugin: Claude REPL ```bash /plugin install full-stack-auth@scalekit-auth-stack ``` 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 Full Stack Auth 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. 3. ## Generate authentication implementation Use a structured prompt to direct the coding agent. A well-formed prompt ensures the agent generates complete, production-ready Full Stack Auth code that includes all required security components. Copy the following prompt into your coding agent: Authentication implementation prompt ```md Guide the coding agent to implement Scalekit full-stack auth — initialize ScalekitClient with environment credentials, implement the login redirect, handle the OAuth callback to exchange the code for tokens, store the session securely, and add a logout endpoint that clears the session. Code only. ``` When you submit this prompt, Claude Code loads the Full Stack Auth skill from the marketplace -> 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 -> adds secure session storage and 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 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. 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. * Codex 1. ## Install the Scalekit Auth Stack marketplace Install Scalekit’s Codex-native marketplace to access focused authentication plugins and reusable implementation guidance. Run the bootstrap installer: Terminal ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` This installer downloads the marketplace from GitHub, installs it into `~/.codex/marketplaces/scalekit-auth-stack`, and only updates `~/.agents/plugins/marketplace.json` when it is safe to do so. If Codex skips your personal marketplace file The installer avoids overwriting another personal marketplace by default. If it skips that file, follow the installer’s manual path and select the marketplace from `~/.codex/marketplaces/scalekit-auth-stack/.agents/plugins/marketplace.json`. 2. ## Enable the Full Stack Auth plugin Restart Codex so it reloads installed marketplaces, then open the Plugin Directory and select **Scalekit Auth Stack**. Install the `full-stack-auth` plugin. This plugin includes the workflows, references, and prompts Codex uses to generate Full Stack Auth code that matches your existing project structure. 3. ## Generate the authentication implementation Use a structured prompt to direct Codex. A well-formed prompt helps Codex generate complete, production-ready Full Stack Auth code that includes the core security components. Copy the following prompt into Codex: Authentication implementation prompt ```md Guide the coding agent to implement Scalekit full-stack auth — initialize ScalekitClient with environment credentials, implement the login redirect, handle the OAuth callback to exchange the code for tokens, store the session securely, and add a logout endpoint that clears the session. Code only. ``` When you submit this prompt, Codex loads the Full Stack Auth plugin from the Scalekit Auth Stack marketplace, 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. ## Add the Scalekit authstack marketplace 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. Register Scalekit’s plugin marketplace to access pre-configured authentication plugins. This marketplace provides implementation skills that help GitHub Copilot generate correct Full Stack Auth code. Terminal ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` The marketplace provides specialized plugins that understand full-stack auth patterns and OAuth 2.0 security requirements. These plugins guide GitHub Copilot to generate implementation code that matches your project structure. 2. ## Install the Full Stack Auth plugin Install the Full Stack Auth plugin to give GitHub Copilot the skills needed to generate complete authentication code: Terminal ```bash copilot plugin install full-stack-auth@scalekit-auth-stack ``` Verify the plugin is installed Confirm the plugin installed successfully: Terminal ```bash copilot plugin list ``` Keep plugins updated Update authentication plugins regularly to receive security patches and improvements: Terminal ```bash copilot plugin update full-stack-auth@scalekit-auth-stack ``` 3. ## Generate authentication implementation Use a structured prompt to direct GitHub Copilot. A well-formed prompt ensures the agent generates complete, production-ready Full Stack Auth code that includes all required security components. Copy the following command into your terminal: Terminal ```bash copilot "Implement Scalekit full-stack auth — initialize ScalekitClient with environment credentials, implement the login redirect, handle the OAuth callback to exchange the code for tokens, store the session securely, and add a logout endpoint that clears the session. Code only." ``` GitHub Copilot uses the Full Stack Auth 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. 4. ## 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 Scalekit Auth Stack is under review on Cursor Marketplace The Scalekit Auth Stack plugin is currently under review and not yet live on [cursor.com/marketplace](https://cursor.com/marketplace). Once approved, you’ll be able to install it directly with an “Add to Cursor” button. Until then, use the local installer to load the plugins into Cursor. 1. ## Install the Scalekit Auth Stack locally Terminal ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/cursor-authstack/main/install.sh | bash ``` This installer downloads the latest Scalekit Cursor plugin bundle and installs each auth plugin into `~/.cursor/plugins/local/`. Use a symlink when iterating locally If you’re developing the plugin repo locally and want changes to show up without recopies, use the local installer path described in the repository README to symlink plugins into `~/.cursor/plugins/local`. 2. ## Reload Cursor and enable the plugin Restart Cursor, or run **Developer: Reload Window**, then open **Settings > Cursor Settings > Plugins**. Select the authentication plugin you need, such as **Full Stack Auth**, **Modular SSO**, or **MCP Auth**, and enable it. Alternatively: Install via Skills CLI You can also install Scalekit skills with the Vercel Skills CLI: Terminal ```bash npx skills add scalekit-inc/skills ``` Use `--list` to browse available skills or `--skill ` to install a specific auth type. Refer to Cursor’s documentation for how to invoke skills once installed. 3. ## Generate the implementation Open Cursor’s chat panel with **Cmd+L** (macOS) or **Ctrl+L** (Windows/Linux) and paste in an implementation prompt. Use the same prompt from the corresponding Claude Code tab — the Scalekit plugins and their authentication skills work identically in Cursor. 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 Once the Scalekit Auth Stack is live on [cursor.com/marketplace](https://cursor.com/marketplace), you’ll be able to skip the local installer and install it directly inside Cursor. * 40+ agents Scalekit skills work with 40+ AI agents via the [Vercel Skills CLI](https://vercel.com/docs/agent-resources/skills). Install skills to add Scalekit authentication to your agent. Supported agents include Claude Code, Cursor, GitHub Copilot CLI, OpenCode, Windsurf, Cline, Gemini CLI, Codex, and 30+ others. 1. ## Install interactively Run the command with no flags to be guided through the available skills: Terminal ```bash npx skills add scalekit-inc/skills ``` 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/skills --list # Install a specific skill npx skills add scalekit-inc/skills --skill adding-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/skills --all --global ``` This installs skills for Full Stack Auth, Agent Auth, MCP Auth, Modular SSO, and Modular SCIM. --- # 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. This guide shows you how to configure these agents so they analyze your codebase, apply consistent authentication patterns, and generate production-ready code that integrates OAuth 2.1 end-to-end, 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. ## Add the Scalekit Auth Stack marketplace Not yet on Claude Code? Follow the [official quickstart guide](https://code.claude.com/docs/en/quickstart) to install it. Register Scalekit’s plugin marketplace to access pre-configured authentication skills. This marketplace provides context-aware prompts and implementation guides that help coding agents generate correct authentication code. Start the Claude Code REPL: Terminal ```bash claude ``` Then add the marketplace: Claude REPL ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` When the marketplace registers successfully, you’ll see confirmation output: Terminal ```bash ❯ /plugin marketplace add scalekit-inc/claude-code-authstack ⎿ Successfully added marketplace: scalekit-auth-stack ``` The marketplace provides specialized authentication plugins that understand MCP server architectures and OAuth 2.1 security requirements. These plugins guide the coding agent to generate implementation code that matches your project structure. 2. ## Enable authentication plugins Select which authentication capabilities to activate in your development environment. Each plugin provides specific skills that the coding agent uses to generate authentication code. Directly install the specific plugin: Claude REPL ```bash /plugin install mcp-auth@scalekit-auth-stack ``` 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=69cce21a4f77360008b1503a) 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. 3. ## 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=%40%2Fassets%2Fdocs%2Fai-assisted-mcp-quickstart%2Fskill-activation.png\&dpl=69cce21a4f77360008b1503a) 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 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 1 # Start your MCP server 2 npm start 3 4 # Test discovery endpoint 5 curl http://localhost:3000/.well-known/oauth-authorization-server 6 7 # Test protected endpoint (should return 401) 8 curl http://localhost:3000/mcp 9 10 # Test with valid token 11 curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/mcp ``` * Python Terminal ```bash 1 # Start your MCP server 2 python server.py 3 4 # Test discovery endpoint 5 curl http://localhost:3000/.well-known/oauth-authorization-server 6 7 # Test protected endpoint (should return 401) 8 curl http://localhost:3000/mcp 9 10 # Test with valid token 11 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 1 # Start your MCP server 2 npm start 3 4 # Test discovery endpoint 5 curl http://localhost:3000/.well-known/oauth-authorization-server 6 7 # Test protected endpoint (should return 401) 8 curl http://localhost:3000/mcp 9 10 # Test with valid token 11 curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/mcp ``` - Cursor Terminal ```bash 1 # Start your MCP server 2 python server.py 3 4 # Test discovery endpoint 5 curl http://localhost:3000/.well-known/oauth-authorization-server 6 7 # Test protected endpoint (should return 401) 8 curl http://localhost:3000/mcp 9 10 # Test with valid token 11 curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/mcp ``` - 40+ agents 1. ## Install the Scalekit Auth Stack marketplace Install Scalekit’s Codex-native marketplace to access focused authentication plugins and reusable implementation guidance. Run the bootstrap installer: Terminal ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` This installer downloads the marketplace from GitHub, installs it into `~/.codex/marketplaces/scalekit-auth-stack`, and only updates `~/.agents/plugins/marketplace.json` when it is safe to do so. If Codex skips your personal marketplace file The installer avoids overwriting another personal marketplace by default. If it skips that file, follow the installer’s manual path and select the marketplace from `~/.codex/marketplaces/scalekit-auth-stack/.agents/plugins/marketplace.json`. 2. ## Enable the MCP Auth plugin Restart Codex so it reloads installed marketplaces, then open the Plugin Directory and select **Scalekit Auth Stack**. Install the `mcp-auth` plugin. This plugin includes the workflows, framework-specific guidance, and references Codex uses to generate OAuth 2.1 protection for remote MCP servers. 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 Scalekit Auth Stack marketplace, 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 1 # Start your MCP server 2 npm start 3 4 # Test discovery endpoint 5 curl http://localhost:3000/.well-known/oauth-authorization-server 6 7 # Test protected endpoint (should return 401) 8 curl http://localhost:3000/mcp 9 10 # Test with valid token 11 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. ## Add the Scalekit authstack marketplace 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. Register Scalekit’s plugin marketplace to access pre-configured authentication plugins. This marketplace provides implementation skills that help GitHub Copilot generate correct MCP server authentication code. Terminal ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` The marketplace provides specialized plugins that understand MCP server architectures and OAuth 2.1 security requirements. These plugins guide GitHub Copilot to generate implementation code that matches your project structure. 2. ## Install the MCP Auth plugin Install the MCP Auth plugin to give GitHub Copilot the skills needed to generate OAuth 2.1 authentication code for MCP servers: Terminal ```bash copilot plugin install mcp-auth@scalekit-auth-stack ``` Verify the plugin is installed Confirm the plugin installed successfully: Terminal ```bash copilot plugin list ``` Keep plugins updated Update authentication plugins regularly to receive security patches and improvements: Terminal ```bash copilot plugin update mcp-auth@scalekit-auth-stack ``` 3. ## 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. 4. ## 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 1 # Start your MCP server 2 npm start 3 4 # Test discovery endpoint 5 curl http://localhost:3000/.well-known/oauth-authorization-server 6 7 # Test protected endpoint (should return 401) 8 curl http://localhost:3000/mcp 9 10 # Test with valid token 11 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 Scalekit Auth Stack is under review on Cursor Marketplace The Scalekit Auth Stack plugin is currently under review and not yet live on [cursor.com/marketplace](https://cursor.com/marketplace). Once approved, you’ll be able to install it directly with an “Add to Cursor” button. Until then, use the local installer to load the plugins into Cursor. 1. ## Install the Scalekit Auth Stack locally Terminal ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/cursor-authstack/main/install.sh | bash ``` This installer downloads the latest Scalekit Cursor plugin bundle and installs each auth plugin into `~/.cursor/plugins/local/`. Use a symlink when iterating locally If you’re developing the plugin repo locally and want changes to show up without recopies, use the local installer path described in the repository README to symlink plugins into `~/.cursor/plugins/local`. 2. ## Reload Cursor and enable the plugin Restart Cursor, or run **Developer: Reload Window**, then open **Settings > Cursor Settings > Plugins**. Select the authentication plugin you need, such as **Full Stack Auth**, **Modular SSO**, or **MCP Auth**, and enable it. Alternatively: Install via Skills CLI You can also install Scalekit skills with the Vercel Skills CLI: Terminal ```bash npx skills add scalekit-inc/skills ``` Use `--list` to browse available skills or `--skill ` to install a specific auth type. Refer to Cursor’s documentation for how to invoke skills once installed. 3. ## Generate the implementation Open Cursor’s chat panel with **Cmd+L** (macOS) or **Ctrl+L** (Windows/Linux) and paste in an implementation prompt. Use the same prompt from the corresponding Claude Code tab — the Scalekit plugins and their authentication skills work identically in Cursor. 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 Once the Scalekit Auth Stack is live on [cursor.com/marketplace](https://cursor.com/marketplace), you’ll be able to skip the local installer and install it directly inside Cursor. - Python Scalekit skills work with 40+ AI agents via the [Vercel Skills CLI](https://vercel.com/docs/agent-resources/skills). Install skills to add Scalekit authentication to your agent. Supported agents include Claude Code, Cursor, GitHub Copilot CLI, OpenCode, Windsurf, Cline, Gemini CLI, Codex, and 30+ others. 1. ## Install interactively Run the command with no flags to be guided through the available skills: Terminal ```bash npx skills add scalekit-inc/skills ``` 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/skills --list # Install a specific skill npx skills add scalekit-inc/skills --skill adding-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/skills --all --global ``` This installs skills for Full Stack Auth, Agent Auth, MCP Auth, Modular SSO, and Modular SCIM. ## 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. This guide shows you how to configure these agents so they analyze your codebase, apply SCIM patterns, and generate production-ready code for user provisioning, deprovisioning, and lifecycle management that follows security best practices and reduces implementation time from hours to minutes. * Claude Code 1. ## Add the Scalekit Auth Stack marketplace Not yet on Claude Code? Follow the [official quickstart guide](https://code.claude.com/docs/en/quickstart) to install it. Register Scalekit’s plugin marketplace to access pre-configured SCIM skills. This marketplace provides context-aware prompts and implementation guides that help coding agents generate correct directory sync code. Start the Claude Code REPL: Terminal ```bash claude ``` Then add the marketplace: Claude REPL ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` When the marketplace registers successfully, you’ll see confirmation output: Terminal ```bash ❯ /plugin marketplace add scalekit-inc/claude-code-authstack ⎿ Successfully added marketplace: scalekit-auth-stack ``` The marketplace provides specialized SCIM plugins that understand directory sync patterns and webhook security requirements. These plugins guide the coding agent to generate implementation code that matches your project structure. 2. ## Enable SCIM plugins Select which directory sync capabilities to activate in your development environment. Each plugin provides specific skills that the coding agent uses to generate SCIM webhook handling code. Directly install the specific plugin: Claude REPL ```bash /plugin install modular-scim@scalekit-auth-stack ``` 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. 3. ## 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. 4. ## 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 Scalekit Auth Stack marketplace Install Scalekit’s Codex-native marketplace to access focused authentication plugins and reusable implementation guidance. Run the bootstrap installer: Terminal ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` This installer downloads the marketplace from GitHub, installs it into `~/.codex/marketplaces/scalekit-auth-stack`, and only updates `~/.agents/plugins/marketplace.json` when it is safe to do so. If Codex skips your personal marketplace file The installer avoids overwriting another personal marketplace by default. If it skips that file, follow the installer’s manual path and select the marketplace from `~/.codex/marketplaces/scalekit-auth-stack/.agents/plugins/marketplace.json`. 2. ## Enable the Modular SCIM plugin Restart Codex so it reloads installed marketplaces, then open the Plugin Directory and select **Scalekit Auth Stack**. Install the `modular-scim` plugin. This plugin includes the workflows, references, and prompts Codex uses to generate SCIM provisioning and deprovisioning code for your application. 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 Scalekit Auth Stack 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, 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. ## Add the Scalekit authstack marketplace 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. Register Scalekit’s plugin marketplace to access pre-configured SCIM plugins. This marketplace provides implementation skills that help GitHub Copilot generate correct directory sync code. Terminal ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` The marketplace provides specialized plugins that understand directory sync patterns and webhook security requirements. These plugins guide GitHub Copilot to generate implementation code that matches your project structure. 2. ## Install the Modular SCIM plugin Install the Modular SCIM plugin to give GitHub Copilot the skills needed to generate SCIM webhook handling code: Terminal ```bash copilot plugin install modular-scim@scalekit-auth-stack ``` Verify the plugin is installed Confirm the plugin installed successfully: Terminal ```bash copilot plugin list ``` Keep plugins updated Update SCIM plugins regularly to receive security patches and improvements: Terminal ```bash copilot plugin update modular-scim@scalekit-auth-stack ``` 3. ## 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. 4. ## 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 Scalekit Auth Stack is under review on Cursor Marketplace The Scalekit Auth Stack plugin is currently under review and not yet live on [cursor.com/marketplace](https://cursor.com/marketplace). Once approved, you’ll be able to install it directly with an “Add to Cursor” button. Until then, use the local installer to load the plugins into Cursor. 1. ## Install the Scalekit Auth Stack locally Terminal ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/cursor-authstack/main/install.sh | bash ``` This installer downloads the latest Scalekit Cursor plugin bundle and installs each auth plugin into `~/.cursor/plugins/local/`. Use a symlink when iterating locally If you’re developing the plugin repo locally and want changes to show up without recopies, use the local installer path described in the repository README to symlink plugins into `~/.cursor/plugins/local`. 2. ## Reload Cursor and enable the plugin Restart Cursor, or run **Developer: Reload Window**, then open **Settings > Cursor Settings > Plugins**. Select the authentication plugin you need, such as **Full Stack Auth**, **Modular SSO**, or **MCP Auth**, and enable it. Alternatively: Install via Skills CLI You can also install Scalekit skills with the Vercel Skills CLI: Terminal ```bash npx skills add scalekit-inc/skills ``` Use `--list` to browse available skills or `--skill ` to install a specific auth type. Refer to Cursor’s documentation for how to invoke skills once installed. 3. ## Generate the implementation Open Cursor’s chat panel with **Cmd+L** (macOS) or **Ctrl+L** (Windows/Linux) and paste in an implementation prompt. Use the same prompt from the corresponding Claude Code tab — the Scalekit plugins and their authentication skills work identically in Cursor. 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 Once the Scalekit Auth Stack is live on [cursor.com/marketplace](https://cursor.com/marketplace), you’ll be able to skip the local installer and install it directly inside Cursor. * 40+ agents Scalekit skills work with 40+ AI agents via the [Vercel Skills CLI](https://vercel.com/docs/agent-resources/skills). Install skills to add Scalekit authentication to your agent. Supported agents include Claude Code, Cursor, GitHub Copilot CLI, OpenCode, Windsurf, Cline, Gemini CLI, Codex, and 30+ others. 1. ## Install interactively Run the command with no flags to be guided through the available skills: Terminal ```bash npx skills add scalekit-inc/skills ``` 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/skills --list # Install a specific skill npx skills add scalekit-inc/skills --skill adding-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/skills --all --global ``` This installs skills for Full Stack Auth, Agent Auth, MCP Auth, Modular SSO, and Modular SCIM. --- # 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. This guide shows you how to configure these agents so they analyze your codebase, apply SSO patterns, and generate production-ready code that integrates enterprise identity providers and follows security best practices while reducing implementation time from hours to minutes. * Claude Code 1. ## Add the Scalekit Auth Stack marketplace Not yet on Claude Code? Follow the [official quickstart guide](https://code.claude.com/docs/en/quickstart) to install it. Register Scalekit’s plugin marketplace to access pre-configured authentication skills. This marketplace provides context-aware prompts and implementation guides that help coding agents generate correct Modular SSO code. Start the Claude Code REPL: Terminal ```bash claude ``` Then add the marketplace: Claude REPL ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` When the marketplace registers successfully, you’ll see confirmation output: Terminal ```bash ❯ /plugin marketplace add scalekit-inc/claude-code-authstack ⎿ Successfully added marketplace: scalekit-auth-stack ``` The marketplace provides specialized authentication plugins that understand SSO patterns and SAML/OIDC security requirements. These plugins guide the coding agent to generate implementation code that matches your project structure. 2. ## Enable authentication plugins Select which authentication capabilities to activate in your development environment. Each plugin provides specific skills that the coding agent uses to generate SSO code. Directly install the specific plugin: Claude REPL ```bash /plugin install modular-sso@scalekit-auth-stack ``` 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. 3. ## 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. 4. ## 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 Scalekit Auth Stack marketplace Install Scalekit’s Codex-native marketplace to access focused authentication plugins and reusable implementation guidance. Run the bootstrap installer: Terminal ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash ``` This installer downloads the marketplace from GitHub, installs it into `~/.codex/marketplaces/scalekit-auth-stack`, and only updates `~/.agents/plugins/marketplace.json` when it is safe to do so. If Codex skips your personal marketplace file The installer avoids overwriting another personal marketplace by default. If it skips that file, follow the installer’s manual path and select the marketplace from `~/.codex/marketplaces/scalekit-auth-stack/.agents/plugins/marketplace.json`. 2. ## Enable the Modular SSO plugin Restart Codex so it reloads installed marketplaces, then open the Plugin Directory and select **Scalekit Auth Stack**. Install the `modular-sso` plugin. This plugin includes the workflows, references, and prompts Codex uses to generate SAML and OIDC SSO code for your existing application. 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 Scalekit Auth Stack 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, 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. ## Add the Scalekit authstack marketplace 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. Register Scalekit’s plugin marketplace to access pre-configured authentication plugins. This marketplace provides implementation skills that help GitHub Copilot generate correct Modular SSO code. Terminal ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` The marketplace provides specialized plugins that understand SSO patterns and SAML/OIDC security requirements. These plugins guide GitHub Copilot to generate implementation code that matches your project structure. 2. ## Install the Modular SSO plugin Install the Modular SSO plugin to give GitHub Copilot the skills needed to generate SSO code: Terminal ```bash copilot plugin install modular-sso@scalekit-auth-stack ``` Verify the plugin is installed Confirm the plugin installed successfully: Terminal ```bash copilot plugin list ``` Keep plugins updated Update authentication plugins regularly to receive security patches and improvements: Terminal ```bash copilot plugin update modular-sso@scalekit-auth-stack ``` 3. ## 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. 4. ## 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 Scalekit Auth Stack is under review on Cursor Marketplace The Scalekit Auth Stack plugin is currently under review and not yet live on [cursor.com/marketplace](https://cursor.com/marketplace). Once approved, you’ll be able to install it directly with an “Add to Cursor” button. Until then, use the local installer to load the plugins into Cursor. 1. ## Install the Scalekit Auth Stack locally Terminal ```bash curl -fsSL https://raw.githubusercontent.com/scalekit-inc/cursor-authstack/main/install.sh | bash ``` This installer downloads the latest Scalekit Cursor plugin bundle and installs each auth plugin into `~/.cursor/plugins/local/`. Use a symlink when iterating locally If you’re developing the plugin repo locally and want changes to show up without recopies, use the local installer path described in the repository README to symlink plugins into `~/.cursor/plugins/local`. 2. ## Reload Cursor and enable the plugin Restart Cursor, or run **Developer: Reload Window**, then open **Settings > Cursor Settings > Plugins**. Select the authentication plugin you need, such as **Full Stack Auth**, **Modular SSO**, or **MCP Auth**, and enable it. Alternatively: Install via Skills CLI You can also install Scalekit skills with the Vercel Skills CLI: Terminal ```bash npx skills add scalekit-inc/skills ``` Use `--list` to browse available skills or `--skill ` to install a specific auth type. Refer to Cursor’s documentation for how to invoke skills once installed. 3. ## Generate the implementation Open Cursor’s chat panel with **Cmd+L** (macOS) or **Ctrl+L** (Windows/Linux) and paste in an implementation prompt. Use the same prompt from the corresponding Claude Code tab — the Scalekit plugins and their authentication skills work identically in Cursor. 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 Once the Scalekit Auth Stack is live on [cursor.com/marketplace](https://cursor.com/marketplace), you’ll be able to skip the local installer and install it directly inside Cursor. * 40+ agents Scalekit skills work with 40+ AI agents via the [Vercel Skills CLI](https://vercel.com/docs/agent-resources/skills). Install skills to add Scalekit authentication to your agent. Supported agents include Claude Code, Cursor, GitHub Copilot CLI, OpenCode, Windsurf, Cline, Gemini CLI, Codex, and 30+ others. 1. ## Install interactively Run the command with no flags to be guided through the available skills: Terminal ```bash npx skills add scalekit-inc/skills ``` 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/skills --list # Install a specific skill npx skills add scalekit-inc/skills --skill adding-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/skills --all --global ``` This installs skills for Full Stack Auth, Agent Auth, MCP Auth, Modular SSO, and Modular SCIM. --- # DOCUMENT BOUNDARY --- # Billing and usage > View your current plan, manage payment methods, and monitor your Scalekit usage. Manage your Scalekit subscription, view invoices, and monitor your usage from the billing section of the dashboard. ## Access billing [Section titled “Access billing”](#access-billing) Navigate to **Dashboard > Settings > Billing** to view your billing information and manage your subscription. ## Current plan [Section titled “Current plan”](#current-plan) View your current subscription plan, including: * **Plan name** - Your current Scalekit plan * **Monthly active users** - Number of unique users who authenticated this month * **Usage limit** - Maximum number of active users included in your plan * **Renewal date** - When your current billing period ends Monitor your usage Keep track of your monthly active users to avoid unexpected overages. Set up alerts to notify you when approaching your plan limit. ## Usage metrics [Section titled “Usage metrics”](#usage-metrics) Track key usage metrics to understand your authentication patterns: | Metric | Description | | ------------------------------ | ------------------------------------------------------- | | **Monthly Active Users (MAU)** | Unique users who authenticate at least once per month | | **Total Organizations** | Number of organizations created across all environments | | **Authentications** | Total number of successful authentication attempts | | **SSO Logins** | Number of logins through enterprise SSO connections | | **Social Logins** | Number of logins through social identity providers | ## Payment methods [Section titled “Payment methods”](#payment-methods) Manage your payment methods for subscription billing: 1. Navigate to **Dashboard > Settings > Billing** 2. Click **Payment methods** in the sidebar 3. Click **Add payment method** 4. Enter your card details or use a saved payment method ### Update payment method [Section titled “Update payment method”](#update-payment-method) To change your default payment method: 1. Click on the payment method card 2. Click **Make default** to set it as your primary payment method 3. Click **Remove** to delete a payment method Keep payment details current Ensure your payment method information is up to date to prevent service interruption. Expired cards may cause authentication failures. ## Invoices [Section titled “Invoices”](#invoices) View and download your invoices for each billing period: 1. Navigate to **Dashboard > Settings > Billing** 2. Click **Invoices** in the sidebar 3. Click on any invoice to view details 4. Click **Download PDF** to save a copy Invoices include a detailed breakdown of your charges, including base subscription fees and any overage charges. ## Upgrade or downgrade plans [Section titled “Upgrade or downgrade plans”](#upgrade-or-downgrade-plans) Change your plan based on your usage needs: 1. Navigate to **Dashboard > Settings > Billing** 2. Click **Change plan** 3. Select a new plan tier 4. Review the changes and confirm Plan changes take effect immediately. Pro-rated charges or credits apply based on your billing cycle. ## Set up alerts [Section titled “Set up alerts”](#set-up-alerts) Configure usage alerts to notify you when approaching your plan limits: 1. Navigate to **Dashboard > Settings > Billing** 2. Click **Usage alerts** in the sidebar 3. Set thresholds for monthly active users 4. Enter email addresses to receive alerts Set alerts early Configure alerts at 75% and 90% of your plan limit to give yourself time to upgrade before hitting overage charges. --- # DOCUMENT BOUNDARY --- # Manage environments > Configure and manage development, staging, and production environments in Scalekit. Scalekit supports multiple environments to help you manage your application development lifecycle. Keep your development, staging, and production configurations separate while maintaining consistent authentication behavior. ## Environment types [Section titled “Environment types”](#environment-types) Scalekit provides three default environments: | Environment | Purpose | | --------------- | ------------------------------------------------------------- | | **Development** | Local development and testing with relaxed security policies | | **Staging** | Pre-production testing that mirrors production configuration | | **Production** | Live environment with strict security policies and monitoring | Use separate environments Keep your development and production environments separate to prevent accidental configuration changes from affecting your live users. ## Access environment settings [Section titled “Access environment settings”](#access-environment-settings) Navigate to **Dashboard > Settings > Environments** to view and manage your environments. Each environment has its own: * Environment ID and URL * API credentials (client ID and secret) * Redirect URLs * Webhook endpoints * Authentication method configurations ## Switch between environments [Section titled “Switch between environments”](#switch-between-environments) Use the environment selector in the top-right corner of the dashboard to switch between environments. Verify your environment Always confirm you’re working in the correct environment before making configuration changes. The dashboard displays the current environment name in the header. ## Configure environment-specific settings [Section titled “Configure environment-specific settings”](#configure-environment-specific-settings) ### Redirect URLs [Section titled “Redirect URLs”](#redirect-urls) Each environment requires its own set of redirect URLs. Configure the appropriate URLs for your application in each environment: * **Development**: `http://localhost:3000/auth/callback` * **Staging**: `https://staging.yourapp.com/auth/callback` * **Production**: `https://yourapp.com/auth/callback` ### API credentials [Section titled “API credentials”](#api-credentials) Each environment uses unique API credentials. Store credentials securely using environment variables: ```bash 1 # Development 2 SCALEKIT_ENVIRONMENT_ID=dev_env_123 3 SCALEKIT_CLIENT_ID=dev_client_abc 4 SCALEKIT_CLIENT_SECRET=dev_secret_xyz 5 6 # Production 7 SCALEKIT_ENVIRONMENT_ID=prod_env_456 8 SCALEKIT_CLIENT_ID=prod_client_def 9 SCALEKIT_CLIENT_SECRET=prod_secret_uvw ``` ### Webhook endpoints [Section titled “Webhook endpoints”](#webhook-endpoints) Configure different webhook endpoints for each environment to test webhook delivery in staging before enabling in production. ## Environment best practices [Section titled “Environment best practices”](#environment-best-practices) * **Never use production credentials in development** * **Test all changes in staging before deploying to production** * **Use environment-specific API endpoints** * **Monitor logs separately for each environment** * **Keep webhook configurations synchronized across environments** --- # DOCUMENT BOUNDARY --- # Manage team members > Invite team members to your Scalekit organization and manage their access and permissions. Scalekit allows you to collaborate with your team by inviting members to your organization. Control who can access your dashboard and what actions they can perform based on their role. ## Access team management [Section titled “Access team management”](#access-team-management) Navigate to **Dashboard > Settings > Team** to view and manage team members. ## Team member roles [Section titled “Team member roles”](#team-member-roles) Scalekit supports two roles with different permission levels: | Role | Permissions | | ---------- | ------------------------------------------------------------------------------------------------- | | **Owner** | Full access to all settings, billing, and team management. Can invite and remove members. | | **Member** | View and manage authentication configurations, but cannot access billing or remove other members. | At least one owner required Your organization must have at least one owner at all times. The last owner cannot leave or change their role. ## Invite team members [Section titled “Invite team members”](#invite-team-members) 1. Navigate to **Dashboard > Settings > Team** 2. Click **Invite member** 3. Enter the team member’s email address 4. Select their role (Owner or Member) 5. Click **Send invite** The invited member receives an email with a link to join your organization. They must sign in with their existing Scalekit account or create a new account to accept the invitation. ## Manage pending invitations [Section titled “Manage pending invitations”](#manage-pending-invitations) View and manage pending invitations from the Team settings page: * **Resend invite** - Send a reminder email for pending invitations * **Cancel invite** - Revoke a pending invitation before it’s accepted ## Change member roles [Section titled “Change member roles”](#change-member-roles) 1. Navigate to **Dashboard > Settings > Team** 2. Find the team member whose role you want to change 3. Click the **Role** dropdown next to their name 4. Select the new role Promote carefully Only grant Owner role to trusted team members who need full access to billing and team management. ## Remove team members [Section titled “Remove team members”](#remove-team-members) 1. Navigate to **Dashboard > Settings > Team** 2. Find the team member you want to remove 3. Click the **Remove** button next to their name 4. Confirm the removal Removed team members immediately lose access to your organization’s dashboard and configurations. ## Team member activity [Section titled “Team member activity”](#team-member-activity) View recent activity for each team member, including: * When they joined the organization * Recent configuration changes they made * Last sign-in time ## Security best practices [Section titled “Security best practices”](#security-best-practices) * **Use the principle of least privilege** - Grant Member role by default * **Regularly review team access** - Remove members who no longer need access * **Monitor audit logs** - Track team member activity in the auth logs * **Enable SSO for team access** - Require SSO authentication for dashboard access --- # DOCUMENT BOUNDARY --- # SCIM Simulator > Test your SCIM integration locally with the Scalekit SCIM Simulator Coming soon --- # 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) ![](/.netlify/images?url=_astro%2Fwindsurf.CfsQQlGb.png\&w=1357\&h=818\&dpl=69cce21a4f77360008b1503a) Windsurf enables `@docs` mentions within the Cascade chat to search for the best answers to your questions. * Full Documentation ```plaintext 1 @docs:https://docs.scalekit.com/llms-full.txt 2 ``` Costs more tokens. * Specific Section ```plaintext 1 @docs:https://docs.scalekit.com/your-specific-section-or-file 2 ``` Costs less tokens. * Let AI decide ```plaintext 1 @docs:https://docs.scalekit.com/llms.txt 2 ``` 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 --- # Authorization best practices > Security guidelines and best practices for implementing robust authorization systems with Scalekit Implementing secure and maintainable authorization requires careful planning and adherence to security best practices. This guide consolidates proven patterns and recommendations for building robust access control systems with Scalekit. ## Permission design principles [Section titled “Permission design principles”](#permission-design-principles) ### Use consistent naming patterns [Section titled “Use consistent naming patterns”](#use-consistent-naming-patterns) **Follow the `resource:action` format consistently** * Group related permissions under common resource names * Use descriptive action names (`create`, `read`, `update`, `delete`, `manage`) * Maintain consistency across your entire application Good permission naming examples ```javascript 1 // Project management permissions 2 "projects:create" // Create new projects 3 "projects:read" // View project details 4 "projects:update" // Modify existing projects 5 "projects:delete" // Remove projects 6 "projects:manage" // Full project administration 7 8 // User management permissions 9 "users:invite" // Send user invitations 10 "users:read" // View user profiles 11 "users:update" // Modify user information 12 "users:suspend" // Temporarily disable users 13 14 // Billing permissions 15 "billing:read" // View billing information 16 "billing:manage" // Modify payment methods and plans ``` ### Keep permissions granular [Section titled “Keep permissions granular”](#keep-permissions-granular) **Create specific permissions for distinct actions** * Avoid overly broad permissions that grant too much access * Consider breaking down complex actions into smaller, specific permissions * Allow for precise control over individual capabilities Granular vs. broad permissions ```javascript 1 // ❌ Too broad - grants excessive access 2 "admin:all" // Dangerous - gives unlimited access 3 4 // ✅ Granular - precise control 5 "users:create" 6 "users:read" 7 "users:update" 8 "users:delete" 9 "billing:read" 10 "billing:update" 11 "settings:read" 12 "settings:update" ``` ### Plan for inheritance [Section titled “Plan for inheritance”](#plan-for-inheritance) **Design permissions that work well when inherited through roles** * Consider permission hierarchies (e.g., `manage` implies `create`, `read`, `update`, `delete`) * Group related permissions that are commonly assigned together * Create logical permission families that make sense for role composition Permission hierarchy design ```javascript 1 // Base permissions 2 "tasks:read" // View tasks 3 "tasks:create" // Create new tasks 4 "tasks:update" // Modify existing tasks 5 "tasks:delete" // Remove tasks 6 7 // Composite permission 8 "tasks:manage" // Implies all above permissions 9 10 // Role composition 11 const viewerRole = ["tasks:read"]; 12 const editorRole = ["tasks:read", "tasks:create", "tasks:update"]; 13 const managerRole = ["tasks:manage"]; // Includes all task permissions ``` ### Document permission purposes [Section titled “Document permission purposes”](#document-permission-purposes) **Use clear, descriptive display names and descriptions** * Provide meaningful descriptions explaining what each permission allows * Maintain documentation of how permissions relate to your application features * Include use cases and security implications in your documentation ## Runtime access control security [Section titled “Runtime access control security”](#runtime-access-control-security) ### Fail securely by default [Section titled “Fail securely by default”](#fail-securely-by-default) **Deny access when permissions are unclear or missing** * Always default to denying access when in doubt * Log access attempts for security auditing and compliance * Use explicit allow-lists rather than deny-lists Secure default patterns ```javascript 1 // ❌ Insecure - fails open 2 function hasPermission(user, permission) { 3 if (!user || !user.permissions) { 4 return true; // Dangerous - grants access when uncertain 5 } 6 return user.permissions.includes(permission); 7 } 8 9 // ✅ Secure - fails closed 10 function hasPermission(user, permission) { 11 if (!user || !user.permissions || !permission) { 12 console.warn('Access denied: Missing user, permissions, or permission check'); 13 return false; // Safe default 14 } 15 return user.permissions.includes(permission); 16 } 17 18 // ✅ Secure with audit logging 19 function hasPermission(user, permission, resource = null) { 20 const granted = user?.permissions?.includes(permission) || false; 21 22 // Log all access attempts for security auditing 23 auditLog({ 24 userId: user?.id, 25 permission, 26 resource, 27 granted, 28 timestamp: new Date().toISOString(), 29 ipAddress: getCurrentRequestIP() 30 }); 31 32 return granted; 33 } ``` ### Centralize authorization logic [Section titled “Centralize authorization logic”](#centralize-authorization-logic) **Create reusable functions for common permission checks** * Keep authorization rules in dedicated modules or services * Avoid duplicating authorization logic across your application * Make authorization logic easy to test and maintain Centralized authorization service ```javascript 1 // ✅ Centralized authorization service 2 class AuthorizationService { 3 static hasPermission(user, permission) { 4 return user?.permissions?.includes(permission) || false; 5 } 6 7 static hasRole(user, role) { 8 return user?.roles?.includes(role) || false; 9 } 10 11 static canManageProject(user, project) { 12 // Centralized business logic for project access 13 return ( 14 this.hasRole(user, 'admin') || 15 project.ownerId === user.id || 16 (project.managers.includes(user.id) && this.hasPermission(user, 'projects:manage')) 17 ); 18 } 19 20 static requirePermission(permission) { 21 return (req, res, next) => { 22 if (!this.hasPermission(req.user, permission)) { 23 return res.status(403).json({ 24 error: `Access denied. Required permission: ${permission}` 25 }); 26 } 27 next(); 28 }; 29 } 30 } 31 32 // Usage across your application 33 app.get('/api/projects/:id', AuthorizationService.requirePermission('projects:read'), getProject); 34 app.post('/api/projects', AuthorizationService.requirePermission('projects:create'), createProject); ``` ### Validate at multiple layers [Section titled “Validate at multiple layers”](#validate-at-multiple-layers) **Implement defense in depth** * Check permissions at the API layer for all requests * Implement additional checks in your business logic * Use database-level permissions where appropriate Multi-layer authorization ```javascript 1 // Layer 1: API middleware 2 app.use('/api/admin/*', requireRole('admin')); 3 4 // Layer 2: Route-level checks 5 app.get('/api/projects/:id', requirePermission('projects:read'), (req, res) => { 6 // Layer 3: Business logic validation 7 const project = getProject(req.params.id); 8 9 if (!canAccessProject(req.user, project)) { 10 return res.status(403).json({ error: 'Access denied to this project' }); 11 } 12 13 res.json(project); 14 }); 15 16 // Layer 4: Database-level security (where possible) 17 async function getProjectsForUser(userId, organizationId) { 18 return await db.query(` 19 SELECT p.* FROM projects p 20 JOIN project_members pm ON p.id = pm.project_id 21 WHERE pm.user_id = ? AND p.organization_id = ? 22 `, [userId, organizationId]); 23 } ``` ### Handle token expiration gracefully [Section titled “Handle token expiration gracefully”](#handle-token-expiration-gracefully) **Provide seamless user experience during token refresh** * Refresh tokens automatically when possible * Provide clear error messages for expired tokens * Redirect users to re-authenticate when refresh fails Graceful token handling ```javascript 1 // Token validation with automatic refresh 2 async function validateAndRefreshToken(req, res, next) { 3 try { 4 const accessToken = getTokenFromRequest(req); 5 6 // Try to validate current token 7 if (await scalekit.validateAccessToken(accessToken)) { 8 req.user = await decodeAccessToken(accessToken); 9 return next(); 10 } 11 12 // Token expired - attempt refresh 13 const refreshToken = getRefreshTokenFromRequest(req); 14 if (refreshToken) { 15 try { 16 const newTokens = await scalekit.refreshAccessToken(refreshToken); 17 18 // Update tokens in response 19 setTokensInResponse(res, newTokens); 20 req.user = await decodeAccessToken(newTokens.accessToken); 21 return next(); 22 23 } catch (refreshError) { 24 // Refresh failed - clear tokens and require re-authentication 25 clearTokensFromResponse(res); 26 return res.status(401).json({ 27 error: 'Session expired. Please log in again.', 28 redirectToLogin: true 29 }); 30 } 31 } 32 33 // No valid tokens available 34 return res.status(401).json({ 35 error: 'Authentication required', 36 redirectToLogin: true 37 }); 38 39 } catch (error) { 40 console.error('Token validation error:', error); 41 return res.status(401).json({ error: 'Authentication failed' }); 42 } 43 } ``` ## Security considerations [Section titled “Security considerations”](#security-considerations) ### Token security [Section titled “Token security”](#token-security) **Always validate tokens on the server side, never trust client-side token validation** * Store access tokens securely and use HTTPS in production * Regularly audit your permission assignments and access patterns * Implement proper token rotation and expiration policies Secure token storage ```javascript 1 // ✅ Secure token storage 2 function storeTokensSecurely(tokens, res) { 3 // Encrypt tokens before storing 4 const encryptedAccessToken = encrypt(tokens.accessToken); 5 const encryptedRefreshToken = encrypt(tokens.refreshToken); 6 7 // Store with secure cookie settings 8 res.cookie('accessToken', encryptedAccessToken, { 9 httpOnly: true, // Prevents JavaScript access 10 secure: true, // HTTPS only 11 sameSite: 'strict', // CSRF protection 12 maxAge: tokens.expiresIn * 1000 13 }); 14 15 res.cookie('refreshToken', encryptedRefreshToken, { 16 httpOnly: true, 17 secure: true, 18 sameSite: 'strict', 19 maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days 20 }); 21 } ``` ### Audit and monitoring [Section titled “Audit and monitoring”](#audit-and-monitoring) **Track authorization decisions for security and compliance** * Log all access attempts, both successful and failed * Monitor for unusual permission usage patterns * Regularly audit user permissions and role assignments * Implement alerts for privileged access usage Authorization auditing ```javascript 1 function auditAuthorizationDecision(user, action, resource, granted, context = {}) { 2 const auditEntry = { 3 timestamp: new Date().toISOString(), 4 userId: user?.id, 5 userEmail: user?.email, 6 organizationId: user?.organizationId, 7 action, 8 resource, 9 granted, 10 userAgent: context.userAgent, 11 ipAddress: context.ipAddress, 12 sessionId: context.sessionId, 13 // Include relevant permissions and roles for analysis 14 userPermissions: user?.permissions || [], 15 userRoles: user?.roles || [] 16 }; 17 18 // Send to your security monitoring system 19 securityLogger.log('authorization_decision', auditEntry); 20 21 // Alert on suspicious patterns 22 if (!granted && isPrivilegedAction(action)) { 23 securityAlerting.checkForSuspiciousActivity(auditEntry); 24 } 25 } ``` ### Performance optimization [Section titled “Performance optimization”](#performance-optimization) **Design authorization checks to be fast and efficient** * Cache user permissions in memory or fast storage * Avoid database lookups during authorization checks * Use Scalekit’s token-based approach to eliminate runtime permission queries Efficient authorization patterns ```javascript 1 // ✅ Fast authorization using token data 2 function hasPermission(user, permission) { 3 // Permissions are already in the decoded token - no DB lookup needed 4 return user.permissions?.includes(permission) || false; 5 } 6 7 // ✅ Cache role hierarchies for complex checks 8 const roleHierarchyCache = new Map(); 9 10 function getUserEffectivePermissions(user) { 11 const cacheKey = `${user.organizationId}:${user.roles.join(',')}`; 12 13 if (roleHierarchyCache.has(cacheKey)) { 14 return roleHierarchyCache.get(cacheKey); 15 } 16 17 // Calculate effective permissions from roles 18 const effectivePermissions = calculateEffectivePermissions(user.roles); 19 roleHierarchyCache.set(cacheKey, effectivePermissions); 20 21 return effectivePermissions; 22 } ``` --- # DOCUMENT BOUNDARY --- # SDKs > Ready-to-use SDKs for Node.js, Python, Go, and Java to integrate Scalekit into your app v2.5.0 • Updated 1 week ago Full-featured, TypeScript-friendly SDK for modern Node.js based applications TypeScript & ESM ready Express, NestJS, Next.js compatible [Get Started →](/sdks/node/) v2.6.0 • Updated 3 weeks ago Async-first design with complete type hints and Pydantic validation Pydantic v2 validated FastAPI, Django, Flask compatible [Get Started →](/sdks/python/) v2.6.0 • Updated 2 days ago Zero-dependency, idiomatic Go SDK for high-performance services Thread-safe & lightweight Gin, Echo, Chi compatible [Get Started →](/sdks/go/) v2.0.11 • Updated 1 month ago Enterprise-ready SDK with seamless Spring Boot integration Spring Boot integrated Maven Central published [Get Started →](/sdks/java/) Official Expo SDK with React Hooks for enterprise-ready mobile authentication React Hooks & TypeScript OAuth 2.0 with PKCE [Get Started →](/sdks/expo/) --- # 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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 1 const options = { 2 organizationId: 'org_32656XXXXXX0438', 3 } 4 5 const authorizationUrl = await scalekit.getAuthorizationUrl( 6 'https://your-app.example.com/auth/callback', 7 options, 8 ) ``` * Python Flask ```python 1 options = { 2 "organizationId": "org_32656XXXXXX0438", 3 } 4 5 authorization_url = scalekit_client.get_authorization_url( 6 "https://your-app.example.com/auth/callback", 7 options, 8 ) ``` * Go Gin ```go 1 options := scalekit.AuthorizationUrlOptions{ 2 OrganizationId: "org_32656XXXXXX0438", 3 } 4 5 authorizationURL, err := scalekitClient.GetAuthorizationUrl( 6 "https://your-app.example.com/auth/callback", 7 options, 8 ) ``` * Java Spring Boot ```java 1 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 2 options.setOrganizationId("org_32656XXXXXX0438"); 3 4 URI authorizationUrl = scalekitClient 5 .authentication() 6 .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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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 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. ![](/.netlify/images?url=_astro%2F01.BGnueJDk.png\&w=1970\&h=915\&dpl=69cce21a4f77360008b1503a) 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 --- # Admin portal > Implement Scalekit's self-serve admin portal to let customers configure 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. This guide shows you how to implement both integration methods. For the broader customer onboarding workflow, see [Onboard enterprise customers](/sso/guides/onboard-enterprise-customers/). ## Generate shareable portal link No-code 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. Navigate to **Dashboard > 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 Programmatic 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` ### 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 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 async function generatePortalLink(organizationId) { 10 const link = await scalekit.organization.generatePortalLink(organizationId); 11 return link.location; // Use as iframe src 12 } ``` * Python Flask ```python 6 collapsed lines 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 def generate_portal_link(organization_id): 11 link = scalekit_client.organization.generate_portal_link(organization_id) 12 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. ### 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. ## 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/). [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 --- # Explore sample apps > Explore sample apps for building an Admin Portal and integrating webhooks. Find code examples to streamline SCIM provisioning and user management. Whether you’re building an Admin Portal or implementing webhooks, we’ve got you covered with practical samples and upcoming language-specific examples. ### Admin Portal [Section titled “Admin Portal”](#admin-portal) Our [admin portal](/guides/admin-portal) sample demonstrates key features and functionality for administrative users. It showcase how the admin portal can be integrated with your application to provide efficient and seamless way for IT admins to configure SCIM Provisioning. [Check out the sample app](https://github.com/scalekit-developers/nodejs-example-apps/tree/main/embed-admin-portal-sample) ### NextJS webhook demo [Section titled “NextJS webhook demo”](#nextjs-webhook-demo) This sample application built with NextJS illustrates the implementation and usage of webhooks in a real-world scenario. It provides a practical example of how to integrate webhook functionality into your projects. [Check out the sample app](https://github.com/scalekit-developers/nextjs-example-apps/tree/main/webhook-events) --- # DOCUMENT BOUNDARY --- # 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. 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. ## 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.ByP-1WUT.png\&w=3066\&h=1779\&dpl=69cce21a4f77360008b1503a) 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%2F2026-02-06-16-15-49.ddPnlHEF.png\&w=3068\&h=1942\&dpl=69cce21a4f77360008b1503a) 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.BQje7ud4.png\&w=3020\&h=1721\&dpl=69cce21a4f77360008b1503a) ### 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](/guides/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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` ### 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 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 async function generatePortalLink(organizationId) { 10 const link = await scalekit.organization.generatePortalLink(organizationId); 11 return link.location; // Use as iframe src 12 } ``` * Python Flask ```python 6 collapsed lines 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 def generate_portal_link(organization_id): 11 link = scalekit_client.organization.generate_portal_link(organization_id) 12 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 our [Webhooks](/reference/webhooks/overview/) documentation 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=69cce21a4f77360008b1503a) 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 related to directory operations in Scalekit. ## Table of contents * [Directory connection events](#directory-connection-events) * [`organization.directory_enabled`](#organizationdirectory_enabled) * [`organization.directory_disabled`](#organizationdirectory_disabled) * [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) *** ## Directory connection events ### `organization.directory_enabled` This webhook is triggered when a directory sync is enabled. The event type is `organization.directory_enabled` organization.directory\_enabled ```json 1 { 2 "environment_id": "env_27758032200925221", 3 "id": "evt_55136848686613000", 4 "object": "Directory", 5 "occurred_at": "2025-01-15T08:55:22.802860294Z", 6 "organization_id": "org_55135410258444802", 7 "spec_version": "1", 8 "type": "organization.directory_enabled", 9 "data": { 10 "directory_type": "SCIM", 11 "enabled": false, 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 | Unique identifier for the directory connection | | `directory_type` | string | The type of directory synchronization | | `enabled` | boolean | Indicates if the directory sync is enabled | | `environment_id` | string | Identifier for the environment | | `last_sync_at` | null | Timestamp of the last synchronization, null if not yet synced | | `organization_id` | string | Identifier for the organization | | `provider` | string | The provider of the directory | | `updated_at` | string | Timestamp of when the configuration was last updated | | `occurred_at` | string | Timestamp of when the event occurred | ### `organization.directory_disabled` This webhook is triggered when a directory sync is disabled. The event type is `organization.directory_disabled` organization.directory\_disabled ```json 1 { 2 "spec_version": "1", 3 "id": "evt_53891640779079756", 4 "type": "organization.directory_disabled", 5 "occurred_at": "2025-01-06T18:45:21.057814Z", 6 "environment_id": "env_53814739859406915", 7 "organization_id": "org_53879494091473415", 8 "object": "Directory", 9 "data": { 10 "directory_type": "SCIM", 11 "enabled": false, 12 "id": "dir_53879621145330183", 13 "organization_id": "org_53879494091473415", 14 "provider": "OKTA", 15 "updated_at": "2025-01-06T18:45:21.04978184Z" 16 } 17 } ``` | Field | Type | Description | | ----------------- | ------- | -------------------------------------------------------------------------------- | | `directory_type` | string | Type of directory protocol used for synchronization | | `enabled` | boolean | Indicates whether the directory synchronization is currently enabled or disabled | | `id` | string | Unique identifier for the directory connection | | `last_sync_at` | string | Timestamp of the most recent directory synchronization | | `organization_id` | string | Unique identifier of the organization associated with this directory | | `provider` | string | Identity provider for the directory connection | | `status` | string | Current status of the directory synchronization process | | `updated_at` | string | Timestamp of the most recent update to the directory connection | | `occurred_at` | string | Timestamp of when the event occurred | ## Directory User Events ### `organization.directory.user_created` This webhook is triggered when a new directory user is created. The event type is `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 "cost_center": "QAUZJUHSTYCN", 12 "custom_attributes": { 13 "mobile_phone_number": "1-579-4072" 14 }, 15 "department": "HNXJPGISMIFN", 16 "division": "MJFUEYJOKICN", 17 "dp_id": "", 18 "email": "flavio@runolfsdottir.co.duk", 19 "employee_id": "AWNEDTILGaIZN", 20 "family_name": "Jaquelin", 21 "given_name": "Dayton", 22 "groups": [ 23 { 24 "id": "dirgroup_12312312312312", 25 "name": "Group Name" 26 } 27 ], 28 "id": "diruser_53891546960887884", 29 "language": "se", 30 "locale": "LLWLEWESPLDC", 31 "name": "QURGUZZDYMFU", 32 "nickname": "DTUODYKGFPPC", 33 "organization": "AUIITQVUQGVH", 34 "organization_id": "org_53879494091473415", 35 "phone_number": "1-579-4072", 36 "preferred_username": "kuntala1233a", 37 "profile": "YMIUQUHKGVAX", 38 "raw_attributes": {}, 39 "title": "FKQBHCWJXZSC", 40 "user_type": "RBQFJSQEFAEH", 41 "zoneinfo": "America/Araguaina", 42 "roles": [ 43 { 44 "role_name": "billing_admin" 45 } 46 ] 47 } 48 } ``` | Field | Type | Description | | -------------------- | ------- | ---------------------------------------------------------------------------------- | | `id` | string | Unique ID of the Directory User | | `organization_id` | string | Unique ID of the Organization to which this directory user belongs | | `dp_id` | string | Unique ID of the User in the Directory Provider (IdP) system | | `preferred_username` | string | Preferred username of the directory user | | `email` | string | Email of the directory user | | `active` | boolean | Indicates if the directory user is active | | `name` | string | Fully formatted name of the directory user | | `roles` | array | Array of roles assigned to the directory user | | `groups` | array | Array of groups to which the directory user belongs | | `given_name` | string | Given name of the directory user | | `family_name` | string | Family name of the directory user | | `nickname` | string | Nickname of the directory user | | `picture` | string | URL of the directory user’s profile picture | | `phone_number` | string | Phone number of the directory user | | `address` | object | Address of the directory user | | `custom_attributes` | object | Custom attributes of the directory user | | `raw_attributes` | object | Raw attributes of the directory user as received from the Directory Provider (IdP) | ### `organization.directory.user_updated` This webhook is triggered when a directory user is updated. The event type is `organization.directory.user_updated` organization.directory.user\_updated ```json 1 { 2 "spec_version": "1", 3 "id": "evt_53891546994442316", 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 "id": "diruser_12312312312312", 11 "organization_id": "org_53879494091473415", 12 "dp_id": "", 13 "preferred_username": "", 14 "email": "john.doe@example.com", 15 "active": true, 16 "name": "John Doe", 17 "roles": [ 18 { 19 "role_name": "billing_admin" 20 } 21 ], 22 "groups": [ 23 { 24 "id": "dirgroup_12312312312312", 25 "name": "Group Name" 26 } 27 ], 28 "given_name": "John", 29 "family_name": "Doe", 30 "nickname": "Jhonny boy", 31 "picture": "https://image.com/profile.jpg", 32 "phone_number": "1234567892", 33 "address": { 34 "postal_code": "64112", 35 "state": "Missouri", 36 "formatted": "123, Oxford Lane, Kansas City, Missouri, 64112" 37 }, 38 "custom_attributes": { 39 "attribute1": "value1", 40 "attribute2": "value2" 41 }, 42 "raw_attributes": {} 43 } 44 } ``` | Field | Type | Description | | -------------------- | ------- | ---------------------------------------------------------------------------------- | | `id` | string | Unique ID of the Directory User | | `organization_id` | string | Unique ID of the Organization to which this directory user belongs | | `dp_id` | string | Unique ID of the User in the Directory Provider (IdP) system | | `preferred_username` | string | Preferred username of the directory user | | `email` | string | Email of the directory user | | `active` | boolean | Indicates if the directory user is active | | `name` | string | Fully formatted name of the directory user | | `roles` | array | Array of roles assigned to the directory user | | `groups` | array | Array of groups to which the directory user belongs | | `given_name` | string | Given name of the directory user | | `family_name` | string | Family name of the directory user | | `nickname` | string | Nickname of the directory user | | `picture` | string | URL of the directory user’s profile picture | | `phone_number` | string | Phone number of the directory user | | `address` | object | Address of the directory user | | `custom_attributes` | object | Custom attributes of the directory user | | `raw_attributes` | object | Raw attributes of the directory user as received from the Directory Provider (IdP) | #### `organization.directory.user_deleted` This webhook is triggered when a directory user is deleted. The event type is `organization.directory.user_deleted` organization.directory.user\_deleted ```json 1 { 2 "spec_version": "1", 3 "id": "evt_53891546994442316", 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 "id": "diruser_12312312312312", 11 "organization_id": "org_12312312312312", 12 "dp_id": "", 13 "email": "john.doe@example.com" 14 } 15 } ``` | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------------------------ | | `id` | string | Unique ID of the Directory User | | `organization_id` | string | Unique ID of the Organization to which this directory user belongs | | `dp_id` | string | Unique ID of the User in the Directory Provider (IdP) system | | `email` | string | Email of the directory user | ## Directory Group Events ### `organization.directory.group_created` This webhook is triggered when a new directory group is created. The event type is `organization.directory.group_created` organization.directory.group\_created ```json 1 { 2 "spec_version": "1", 3 "id": "evt_38862741515010639", 4 "environment_id": "env_32080745237316098", 5 "object": "DirectoryGroup", 6 "occurred_at": "2024-09-25T02:26:39.036398577Z", 7 "organization_id": "org_38609339635728478", 8 "type": "organization.directory.group_created", 9 "data": { 10 "directory_id": "dir_38610496391217780", 11 "display_name": "Avengers", 12 "external_id": null, 13 "id": "dirgroup_38862741498233423", 14 "organization_id": "org_38609339635728478", 15 "raw_attributes": {} 16 } 17 } ``` | Field | Type | Description | | ----------------- | ------ | --------------------------------------------------------- | | `directory_id` | string | Unique identifier for the directory | | `display_name` | string | Display name of the directory group | | `external_id` | null | External identifier for the group, null if not specified | | `id` | string | Unique identifier for the directory group | | `organization_id` | string | Identifier for the organization associated with the group | | `raw_attributes` | object | Raw attributes of the directory provider | ### `organization.directory.group_updated` This webhook is triggered when a directory group is updated. The event type is `organization.directory.group_updated` organization.directory.group\_updated ```json 1 { 2 "spec_version": "1", 3 "id": "evt_38864948910162368", 4 "organization_id": "org_38609339635728478", 5 "type": "organization.directory.group_updated", 6 "environment_id": "env_32080745237316098", 7 "object": "DirectoryGroup", 8 "occurred_at": "2024-09-25T02:48:34.745030921Z", 9 "data": { 10 "directory_id": "dir_38610496391217780", 11 "display_name": "Avengers", 12 "external_id": "", 13 "id": "dirgroup_38862741498233423", 14 "organization_id": "org_38609339635728478", 15 "raw_attributes": {} 16 } 17 } ``` | Field | Type | Description | | ----------------- | ------ | --------------------------------------------------------- | | `directory_id` | string | Unique identifier for the directory | | `display_name` | string | Display name of the directory group | | `external_id` | null | External identifier for the group, null if not specified | | `id` | string | Unique identifier for the directory group | | `organization_id` | string | Identifier for the organization associated with the group | | `raw_attributes` | object | Raw attributes of the directory group | ### `organization.directory.group_deleted` This webhook is triggered when a directory group is deleted. The event type is `organization.directory.group_deleted` organization.directory.group\_deleted ```json 1 { 2 "spec_version": "1", 3 "id": "evt_40650399597723966", 4 "environment_id": "env_12205603854221623", 5 "object": "DirectoryGroup", 6 "occurred_at": "2024-10-07T10:25:26.289331747Z", 7 "organization_id": "org_39802449573184223", 8 "type": "organization.directory.group_deleted", 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 } ``` | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------------------------- | | `directory_id` | string | Unique identifier for the directory | | `display_name` | string | Display name of the directory group | | `dp_id` | string | Unique identifier for the group in the directory provider system | | `id` | string | Unique identifier for the directory group | | `organization_id` | string | Identifier for the organization associated with the group | | `raw_attributes` | object | Raw attributes of the directory group as received from the provider | --- # DOCUMENT BOUNDARY --- # Just-in-time provisioning > Automatically provision users when they sign in through SSO for the first time Just-in-time (JIT) provisioning automatically creates users and organization memberships when they sign in through SSO for the first time. This feature allows users to access your application without requiring manual invitations from IT administrators. For example, users don’t need to remember separate credentials or go through additional signup steps - they just sign in through their familiar SSO portal. Your app signs them up instantly. ## Introduction [Section titled “Introduction”](#introduction) JIT provisioning is particularly useful for enterprise customers who want to provide seamless access to your application for their employees while maintaining security and control through their identity provider. When a user signs in through SSO for the first time, Scalekit automatically: 1. **Detects the verified domain** - Scalekit checks if the user’s email domain matches a verified domain in the organization 2. **Creates the user account** - A new user profile is created using information from the identity provider 3. **Establishes membership** - The user is automatically added as a member of the organization 4. **Completes authentication** - The user is signed in and redirected to your application This process happens seamlessly in the background, providing immediate access without manual intervention. ## Enabling JIT provisioning [Section titled “Enabling JIT provisioning”](#enabling-jit-provisioning) JIT provisioning must be enabled for each organization that wants to use this feature. You can enable it through the Scalekit Dashboard or programmatically using the API. ### Enable via Dashboard Coming soon [Section titled “Enable via Dashboard ”](#enable-via-dashboard) 1. Log in to your [Scalekit Dashboard](https://app.scalekit.com). 2. Navigate to **Organizations** and select the organization. 3. Go to **Settings** and find the **JIT Provisioning** section. 4. Toggle the setting to enable JIT provisioning for this organization. ### Enable via API [Section titled “Enable via API”](#enable-via-api) You can also enable JIT provisioning programmatically using the Scalekit API: * 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` Enable JIT provisioning ```javascript 1 // Coming soon - API to enable JIT provisioning ``` ## Domain verification requirement [Section titled “Domain verification requirement”](#domain-verification-requirement) JIT provisioning only works for users whose email domains have been verified by the organization. This ensures that only legitimate members of the organization can automatically gain access to your application. **Organization admins** can verify domains through the [admin portal](/guides/admin-portal/). Once verified, any user with an email address from that domain can use JIT provisioning when signing in through SSO. Note Learn more about [domain verification](/sso/guides/onboard-enterprise-customers/) in the Enterprise SSO guide. ## What’s next? [Section titled “What’s next?”](#whats-next) * Learn about [Allowed Email Domains](/authenticate/manage-users-orgs/email-domain-rules/) for non-SSO authentication methods * Explore [Enterprise SSO](/sso/guides/onboard-enterprise-customers/) setup and configuration * Set up [organization switching](/authenticate/manage-users-orgs/organization-switching/) for users who belong to multiple organizations --- # 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. ![](/.netlify/images?url=_astro%2Flogin.BMj6tPVW.png\&w=3016\&h=1616\&dpl=69cce21a4f77360008b1503a) ## 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 --- # Create and manage organizations > Create and manage organizations in Scalekit, configure settings, and enable enterprise features. Organizations are the foundation of your B2B application, representing your customers and their teams. In Scalekit, organizations serve as multi-tenant containers that isolate user data, configure authentication methods, and manage enterprise features like Single Sign-On (SSO) and directory synchronization. This guide shows you how to create and manage organizations programmatically and through the Scalekit dashboard. ## Understanding organizations [Section titled “Understanding organizations”](#understanding-organizations) Users can belong to multiple organizations with the same identity. This is common in products like Notion, where users collaborate across multiple workspaces. Note You can [customize](/authenticate/fsa/user-management-settings/#organization-meta-name) the terminology to match your product. Organizations can be relabeled as “Workspaces,” “Teams,” or any term that makes sense for your users. ## Create an organization [Section titled “Create an organization”](#create-an-organization) Organizations can be created automatically during user sign-up or programmatically through the API. When users sign up for your application, Scalekit creates a new organization and adds the user to it automatically. For more control over the organization creation process, create organizations programmatically: * 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` - Node.js Create organization ```javascript 1 const organization = await scalekit.organization.createOrganization('Acme Corporation', { 2 externalId: 'acme-corp-123', 3 }); 4 5 console.log('Organization created:', organization.id); ``` - Python Create organization ```python 1 from scalekit.v1.organizations.organizations_pb2 import CreateOrganization 2 3 organization = scalekit_client.organization.create_organization( 4 CreateOrganization( 5 display_name='Acme Corporation', 6 external_id='acme-corp-123', 7 metadata={ 8 'plan': 'enterprise', 9 'industry': 'technology' 10 } 11 ) 12 ) 13 14 print(f'Organization created: {organization.id}') ``` - Go Create organization ```go 1 organization, err := scalekitClient.Organization.CreateOrganization( 2 ctx, 3 "Acme Corporation", 4 scalekit.CreateOrganizationOptions{ 5 ExternalId: "acme-corp-123", 6 }, 7 ) 8 if err != nil { 9 log.Fatal(err) 10 } 11 12 fmt.Printf("Organization created: %s\n", organization.ID) ``` - Java Create organization ```java 1 import java.util.Map; 2 import java.util.HashMap; 3 4 Map metadata = new HashMap<>(); 5 metadata.put("plan", "enterprise"); 6 metadata.put("industry", "technology"); 7 8 CreateOrganization createOrg = CreateOrganization.newBuilder() 9 .setDisplayName("Acme Corporation") 10 .setExternalId("acme-corp-123") 11 .build(); 12 13 Organization organization = scalekitClient.organizations().create(createOrg); 14 System.out.println("Organization created: " + organization.getId()); ``` **External ID**: An optional field to associate the organization with an ID from your system. This is useful for linking Scalekit organizations with records in your own database. ## Update organization details [Section titled “Update organization details”](#update-organization-details) Organization administrators often need to make changes after the initial setup. Typical examples include: * Renaming the organization after a corporate re-brand. * Uploading or replacing the company logo shown on your dashboard or invoices. * Storing metadata your application needs at runtime—such as a billing plan identifier, Stripe customer ID, or internal account reference. - Node.js Update organization ```javascript 1 const updatedOrganization = await scalekit.organization.updateOrganization( 2 'org_12345', 3 { 4 displayName: 'Acme Corporation Ltd', 5 metadata: { 6 plan: 'enterprise', 7 paymentMethod: 'stripe', 8 customField: 'custom-value' 9 } 10 } 11 ); ``` - Python Update organization ```python 1 updated_organization = scalekit_client.organization.update_organization( 2 organization_id='org_12345', 3 organization= UpdateOrganization( 4 display_name='Acme Corporation Ltd', 5 metadata={ 6 'plan': 'enterprise', 7 'payment_method': 'stripe', 8 'custom_field': 'custom-value' 9 } 10 ) 11 ) ``` - Go Update organization ```go 1 metadata := map[string]interface{}{ 2 "plan": "enterprise", 3 "payment_method": "stripe", 4 "custom_field": "custom-value", 5 } 6 7 update := &scalekit.UpdateOrganization{ 8 DisplayName: "Acme Corporation Ltd", 9 Metadata: metadata, 10 } 11 12 updatedOrganization, err := scalekitClient.Organization.UpdateOrganization(ctx, "org_12345", update) ``` - Java Update organization ```java 1 Map metadata = new HashMap<>(); 2 metadata.put("plan", "enterprise"); 3 metadata.put("payment_method", "stripe"); 4 metadata.put("custom_field", "custom-value"); 5 6 UpdateOrganization updateOrganization = UpdateOrganization.newBuilder() 7 .setDisplayName("Acme Corporation Ltd") 8 .putAllMetadata(metadata) 9 .build(); 10 11 Organization updatedOrganization = scalekitClient.organizations() 12 .updateById("org_12345", updateOrganization); ``` **Metadata**: Store additional information about the organization, such as subscription plans, payment methods, or any custom data relevant to your application. ## Configure organization features [Section titled “Configure organization features”](#configure-organization-features) Enable enterprise features for your organizations to support authentication methods like SSO and user provisioning through SCIM. * Node.js Enable organization features ```javascript 1 const settings = { 2 features: [ 3 { 4 name: 'sso', 5 enabled: true, 6 }, 7 { 8 name: 'dir_sync', 9 enabled: true, 10 }, 11 ], 12 }; 13 14 await scalekit.organization.updateOrganizationSettings( 15 'org_12345', 16 settings 17 ); ``` * Python Enable organization features ```python 1 settings = [ 2 {"sso": True}, 3 {"dir_sync": True}, 4 ] 5 6 scalekit_client.organization.update_organization_settings( 7 'org_12345', 8 settings 9 ) ``` * Go Enable organization features ```go 1 settings := scalekit.OrganizationSettings{ 2 Features: []scalekit.OrganizationSettingsFeature{ 3 {Name: "sso", Enabled: true}, 4 {Name: "dir_sync", Enabled: true}, 5 }, 6 } 7 8 _, err := scalekitClient.Organization.UpdateOrganizationSettings( 9 ctx, 10 "org_12345", 11 settings, 12 ) ``` * Java Enable organization features ```java 1 List settings = Arrays.asList( 2 OrganizationSettingsFeature.newBuilder() 3 .setName("sso") 4 .setEnabled(true) 5 .build(), 6 OrganizationSettingsFeature.newBuilder() 7 .setName("dir_sync") 8 .setEnabled(true) 9 .build() 10 ); 11 12 scalekitClient.organizations().updateOrganizationSettings( 13 "org_12345", 14 settings 15 ); ``` ### 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: ![](/.netlify/images?url=_astro%2Flimit-org-users.F8VX5klf.png\&w=2454\&h=618\&dpl=69cce21a4f77360008b1503a) 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. ### Admin Portal access (self-serve configuration) [Section titled “Admin Portal access (self-serve configuration)”](#admin-portal-access-self-serve-configuration) Enterprise customers usually want to manage SSO and directory sync on their own, without involving your support team. Scalekit provides an **Admin Portal** that you can surface to IT administrators in two ways: 1. **Generate a shareable link** and send it via email or chat. 2. **Embed the portal** inside your own settings page with an ` ``` Embed the portal in your application’s settings or admin section where customers manage authentication configuration. ### Configuration and session [Section titled “Configuration and session”](#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. ## Customize the admin portal [Section titled “Customize the admin portal”](#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/). [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 --- # Authenticate with Scalekit API > Learn how to authenticate your server applications with Scalekit API using OAuth 2.0 Client Credentials flow This guide explains how to authenticate your server applications with the Scalekit API using the OAuth 2.0 Client Credentials flow. After reading this guide, you’ll be able to: * Generate an access token using your API credentials * Make authenticated API requests to Scalekit endpoints * Handle authentication errors appropriately This guide targets developers who need to integrate Scalekit services into their backend applications or automate tasks through API calls. ## Before you begin [Section titled “Before you begin”](#before-you-begin) Before starting the authentication process, ensure you have set up your Scalekit account and obtained your API credentials. ## Step 1: Configure your environment [Section titled “Step 1: Configure your environment”](#step-1-configure-your-environment) Store your API credentials securely as environment variables: Environment variables ```sh 1 SCALEKIT_ENVIRONMENT_URL="" 2 SCALEKIT_CLIENT_ID="" 3 SCALEKIT_CLIENT_SECRET="" ``` ## Step 2: Request an access token [Section titled “Step 2: Request an access token”](#step-2-request-an-access-token) To authenticate your API requests, you must first obtain an access token from the Scalekit authorization server. ### Token endpoint URL [Section titled “Token endpoint URL”](#token-endpoint-url) Token endpoint URL ```sh 1 https:///oauth/token ``` ### Send a token request [Section titled “Send a token request”](#send-a-token-request) Choose your preferred method to request an access token: * cURL ```bash 1 curl -X POST \ 2 "https:///oauth/token" \ 3 -H "Content-Type: application/x-www-form-urlencoded" \ 4 -d "grant_type=client_credentials" \ 5 -d "client_id=" \ 6 -d "client_secret=" \ 7 -d "scope=openid profile email" ``` * Node.js ```javascript 1 import axios from 'axios'; 2 3 const config = { 4 clientId: process.env.SCALEKIT_CLIENT_ID, 5 clientSecret: process.env.SCALEKIT_CLIENT_SECRET, 6 tokenUrl: `${process.env.SCALEKIT_ENVIRONMENT_URL}/oauth/token`, 7 scope: 'openid email profile', 8 }; 9 10 async function getClientCredentialsToken() { 11 try { 12 const params = new URLSearchParams(); 13 params.append('grant_type', 'client_credentials'); 14 params.append('client_id', config.clientId); 15 params.append('client_secret', config.clientSecret); 16 17 if (config.scope) { 18 params.append('scope', config.scope); 19 } 20 21 const response = await axios.post(config.tokenUrl, params, { 22 headers: { 23 'Content-Type': 'application/x-www-form-urlencoded', 24 }, 25 }); 26 27 const { access_token, expires_in } = response.data; 28 console.log(`Token acquired successfully. Expires in ${expires_in} seconds.`); 29 return access_token; 30 } catch (error) { 31 console.error('Error getting client credentials token:', error); 32 throw new Error('Failed to obtain access token'); 33 } 34 } ``` * Python ```python 1 import os 2 import json 3 import requests 4 5 def get_access_token(): 6 """Request an access token using client credentials.""" 7 headers = {"Content-Type": "application/x-www-form-urlencoded"} 8 params = { 9 "grant_type": "client_credentials", 10 "client_id": os.environ['SCALEKIT_CLIENT_ID'], 11 "client_secret": os.environ['SCALEKIT_CLIENT_SECRET'] 12 } 13 oauth_token_url = os.environ['SCALEKIT_ENVIRONMENT_URL'] 14 15 response = requests.post(oauth_token_url, headers=headers, data=params, verify=True) 16 access_token = response.json().get('access_token') 17 return access_token ``` ### Understand the token response [Section titled “Understand the token response”](#understand-the-token-response) When your request succeeds, the server returns a JSON response with the following fields: | Field | Description | | -------------- | ----------------------------------------------------- | | `access_token` | The token you’ll use to authenticate API requests | | `token_type` | The token type (always Bearer for this flow) | | `expires_in` | Token validity period in seconds (typically 24 hours) | | `scope` | The authorized scopes for this token | Example token response: Token response ```json 1 { 2 "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNua181Ok4OTEyMjU2NiIsInR5cCI6IkpXVCJ9...", 3 "token_type": "Bearer", 4 "expires_in": 86399, 5 "scope": "openid" 6 } ``` ## Step 3: Make authenticated API requests [Section titled “Step 3: Make authenticated API requests”](#step-3-make-authenticated-api-requests) After obtaining an access token, add it to the `Authorization` header in your API requests. * cURL ```bash 1 curl --request GET "https:///api/v1/organizations" \ 2 -H "Content-Type: application/json" \ 3 -H "Authorization: Bearer " ``` * Node.js (axios) ```javascript 1 async function makeAuthenticatedRequest(endpoint) { 2 try { 3 const access_token = await getClientCredentialsToken(); 4 const url = `${process.env.SCALEKIT_ENVIRONMENT_URL}${endpoint}`; 5 6 const response = await axios.get(url, { 7 headers: { 8 Authorization: `Bearer ${access_token}`, 9 }, 10 }); 11 12 console.log('API Response:', response.data); 13 return response.data; 14 } catch (error) { 15 console.error('Error making authenticated request:', error); 16 throw error; 17 } 18 } ``` * Python (requests) ```python 1 import os 2 import json 3 import requests 4 5 env_url = os.environ['SCALEKIT_ENVIRONMENT_URL'] 6 7 def get_access_token(): 8 """Request an access token using client credentials.""" 9 headers = {"Content-Type": "application/x-www-form-urlencoded"} 10 params = { 11 "grant_type": "client_credentials", 12 "client_id": os.environ['SCALEKIT_CLIENT_ID'], 13 "client_secret": os.environ['SCALEKIT_CLIENT_SECRET'] 14 } 15 16 response = requests.post( 17 url=f"{env_url}/oauth/token", 18 headers=headers, 19 data=params, 20 verify=True) 21 22 access_token = response.json().get('access_token') 23 return access_token 24 25 def get_organizations(get_orgs_endpoint): 26 """Retrieve all organizations for the specified environment.""" 27 access_token = get_access_token() 28 headers = {"Authorization": f"Bearer {access_token}"} 29 30 response = requests.get( 31 url=f"{env_url}/{get_orgs_endpoint}", 32 headers=headers) 33 return response ``` Example API response ```json 1 { 2 "next_page_token": "", 3 "total_size": 3, 4 "organizations": [ 5 { 6 "id": "org_64444217115541813", 7 "create_time": "2025-03-20T13:55:46.690Z", 8 "update_time": "2025-03-21T05:55:03.416772Z", 9 "display_name": "Looney Corp", 10 "region_code": "US", 11 "external_id": "my_unique_id", 12 "metadata": {} 13 } 14 ], 15 "prev_page_token": "" 16 } ``` ## Common authentication issues [Section titled “Common authentication issues”](#common-authentication-issues) | Issue | Possible cause | Solution | | ---------------- | ------------------------ | ------------------------------- | | 401 Unauthorized | Invalid or expired token | Generate a new access token | | 403 Forbidden | Insufficient permissions | Check client credentials scopes | | Connection error | Network or server issue | Retry with exponential backoff | ## Next steps [Section titled “Next steps”](#next-steps) Now that you can authenticate with the Scalekit API, you can: * Browse the complete API reference to discover available endpoints * Create a token management service to handle token refreshing * Implement error handling strategies for production use --- # DOCUMENT BOUNDARY --- # Best practices for client secrets > Learn best practices for managing Scalekit client secrets, including secure storage, rotation procedures, and access control to protect your SSO implementation. Client ID and Client Secret are a form of API credentials, like a username and password. You are responsible for keeping Client Secrets safe and secure. Below are some best practices for how you can keep your secrets safe and how you can leverage some of the functionality offered by us to help you do the same. **Store secrets securely** Whenever a client secret is generated from the Scalekit Dashboard, it is shown only once and cannot be recovered. Therefore, it should be immediately stored in a secure Key Management System (KMS), which offers encryption and access control features. It is crucial not to leave a duplicate copy of the key in the local file. **Avoid insecure sharing** Sharing of secret keys through insecure channels, such as emails, Slack, or customer support messages, should be strictly avoided. **Prevent hardcoding** Storing client secrets within source code as hardcoded strings should be avoided. Instead, store them in your properties file or environments file. These files should not be checked into your source code repository. **Establish rotation procedures** Establishing a Standard Operating Procedure (SOP) for rotating Client Secrets can help in case of accidental secret leakage. Having such procedures in place will ensure a swift and effective response to emergencies, minimizing business impact. **Control access** Access to create, update, or read keys should be granted only to those individuals who require it for their roles. Regularly auditing access can prevent excess privilege allocation. **Monitor usage** Regular monitoring of API logs is recommended to identify potential misuse of API keys early. Developers should avoid using live mode keys when a test mode key is suitable. **Respond to incidents** If suspicious activity is detected or a secret leak is suspected, the current secret should be immediately revoked from the Scalekit Dashboard, and a new one should be generated. In case of uncertainty, it is better to generate a new secret and revoke the existing one. --- # 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=69cce21a4f77360008b1503a) 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 --- # How to register a callback endpoint > Learn how to register a callback endpoint in the Scalekit dashboard. In the authentication flow for a user, a callback endpoint is the endpoint that Scalekit remembers about your application, trusts it, and sends a authentication grant (code). It further expects your application to exchange the code for a user token and user profile. This needs to be pre-registered in the Scalekit dashboard. Go to **Dashboard** > **Authentication** > **Redirect URLS** > **Allowed Callback URLs** and add the callback endpoint. ![](/.netlify/images?url=_astro%2Fallowed-callback-url.CR8LStEH.png\&w=2514\&h=900\&dpl=69cce21a4f77360008b1503a) 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 | 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. --- # 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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. Events are processed in the order they were created. | | **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. Retries continue up to 4 attempts with increasing delays between retries. | 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**: After 1 minute * **Attempt 3**: After 5 minutes * **Attempt 4**: After 15 minutes After the final retry attempt fails, 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. Best practices for webhook reliability Ensure your webhook endpoint responds quickly (within 10 seconds), returns appropriate 2xx status codes for successful processing, and implements idempotency to safely handle duplicate deliveries during retries. --- # DOCUMENT BOUNDARY --- # Custom email templates > Customize authentication email templates with your branding and content Scalekit uses default templates to send authentication emails to your users. You can customize these templates with your own branding and content to provide a consistent experience. Find these templates in **Emails** > **Templates**. ![](/.netlify/images?url=_astro%2Fcustom-templates-list.Bm_WnAfo.png\&w=2852\&h=1592\&dpl=69cce21a4f77360008b1503a) Select one of the listed templates and choose between Scalekit’s default templates or your own custom templates. ![](/.netlify/images?url=_astro%2Fsub-selection-custom-tempaltes.BCgqsBiR.png\&w=2856\&h=1612\&dpl=69cce21a4f77360008b1503a) Select how each email is generated: * **Use Scalekit template**: Preview subject and bodies; you cannot edit them. Emails use Scalekit’s default content. * **Use custom template**: Edit the subject, HTML body, and plain text body. Your saved content is used for future sends. Requires you to [bring your own email provider](/guides/passwordless/custom-email-provider/). ## 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: HTML is shown in capable clients, and the plain text part is used as a fallback where HTML is not supported. Tip Include a clear call-to-action link in the plain text body when using tracking pixels or richly styled buttons in HTML. Once saved, all subsequent emails will use your customized templates. ## Built-in template variables [Section titled “Built-in template variables”](#built-in-template-variables) Use these built-in variables in your templates. Values are injected 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}}` | Organization display name configured in Scalekit | #### Organization [Section titled “Organization”](#organization) Organization variables describe the organization that the user belongs to and are consistent across emails for that organization. | Variable | Description | | ----------------------- | --------------------- | | `{{organization_name}}` | The organization name | #### 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 apply only to the current template. They change per template or send (for example, OTP, magic link, or expiry). For example, `{{link}}` is maybe the same label in both sign up and log in scenarios using magic link. | Variable | Description | | -------------------------- | ----------------------------------------------------------------------------- | | `{{link}}` | Authentication link (magic link or sign up) | | `{{otp}}` | One-time passcode for the current request | | `{{expiry_time_relative}}` | Human-readable relative date format (for example, “14 days, 6 hours, 50 min”) | ## JET template syntax [Section titled “JET template syntax”](#jet-template-syntax) Custom email templates use JET (Just Enough Templates) syntax for dynamic content. JET provides powerful templating features including conditionals, loops, and filters. Here are two common patterns you can use in your email templates: * 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 complete JET syntax documentation including all available functions, filters, and control structures, see the [JET syntax reference](https://github.com/CloudyKit/jet/blob/master/docs/syntax.md). ## Inject you own variables at runtime Passwordless [Section titled “Inject you own variables at runtime ”](#inject-you-own-variables-at-runtime) For more advanced personalization, you can use template variables to include values programatically in the emails. You must be using the Passwordless Headless API for authentication. * Each variable must be a key-value pair. * Maximum of 30 variables per template. * All template variables must have corresponding values in the request. * Avoid using reserved names: `otp`, `expiry_time_relative`, `link`, `expire_time`, `expiry_time`. 1. Create your email template with variables: Example email template ```html

Hello {{ first_name }},

Welcome to {{ company_name }}.

Find your onboarding kit: {{ onboarding_resources }}

``` 2. Include variable values in your authentication request: Authentication request ```js const sendResponse = await scalekit.passwordless.sendPasswordlessEmail( "", { templateVariables: { first_name: "John", company_name: "Acme Corp", onboarding_resources: "https://acme.com/onboarding" } } ); ``` 3. The sent email will include the replaced values: Example email preview ```html Hello John, Welcome to Acme Corp. Find your onboarding kit: https://acme.com/onboarding ``` Caution The API will return a 400 status code if your template references any variables that aren’t provided in the request. *** **Test your knowledge with a quiz** Which choice requires using your own email provider? * Use Scalekit template * Preview subject and bodies * Use custom template * Enable table of contents Submit --- # DOCUMENT BOUNDARY --- # Configure initiate login endpoint > Set up a login endpoint that Scalekit redirects to when users access your application through indirect entry points In certain scenarios, Scalekit redirects users to your application’s login endpoint using OIDC third-party initiated login. Your application must implement this endpoint to construct the authorization URL and redirect users to Scalekit’s authentication flow. Scalekit redirects to your login endpoint in these (example) scenarios: * **Bookmarked login page**: Users bookmark your login page and visit it later. When they access the bookmarked URL, Scalekit redirects them to your application’s login endpoint because the original authentication transaction has expired. * **Password reset completion**: After users complete a password reset, Scalekit redirects them to your login endpoint. Users can then sign in with their new password. * **Email verification completion**: After users verify their email address during signup, Scalekit redirects them to your login endpoint to complete authentication. * **Organization invitations**: When users click an invitation link to join an organization, Scalekit redirects them to your login endpoint with invitation parameters. Your application must forward these parameters to Scalekit’s authorization endpoint. * **Disabled cookies**: If users navigate to Scalekit’s authorization endpoint with cookies disabled, Scalekit redirects them to your login endpoint. ## Configure the initiate login endpoint [Section titled “Configure the initiate login endpoint”](#configure-the-initiate-login-endpoint) Register your login endpoint in the Scalekit dashboard. Go to **Dashboard** > **Authentication** > **Redirect URLs** > **Initiate Login URL** and add your endpoint. ![](/.netlify/images?url=_astro%2Fadd-initiate-login-url.BsYwkIJr.png\&w=2948\&h=524\&dpl=69cce21a4f77360008b1503a) The endpoint must: * Use HTTPS (required in production) * Not point to localhost (production only) * Accept query parameters that Scalekit appends ## Implement the login endpoint [Section titled “Implement the login endpoint”](#implement-the-login-endpoint) Create a `/login` endpoint that constructs the authorization URL and redirects users to Scalekit. * Node.js routes/auth.js ```javascript 1 // Handle indirect auth entry points 2 app.get('/login', (req, res) => { 3 const redirectUri = 'http://localhost:3000/auth/callback'; 4 const options = { 5 scopes: ['openid', 'profile', 'email', 'offline_access'] 6 }; 7 8 const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options); 9 res.redirect(authorizationUrl); 10 }); ``` * Python routes/auth.py ```python 3 collapsed lines 1 from flask import redirect 2 from scalekit import AuthorizationUrlOptions 3 4 # Handle indirect auth entry points 5 @app.route('/login') 6 def login(): 7 redirect_uri = 'http://localhost:3000/auth/callback' 8 options = AuthorizationUrlOptions( 9 scopes=['openid', 'profile', 'email', 'offline_access'] 10 ) 11 12 authorization_url = scalekit_client.get_authorization_url(redirect_uri, options) 13 return redirect(authorization_url) ``` * Go routes/auth.go ```go 1 // Handle indirect auth entry points 2 r.GET("/login", func(c *gin.Context) { 3 redirectUri := "http://localhost:3000/auth/callback" 4 options := scalekitClient.AuthorizationUrlOptions{ 5 Scopes: []string{"openid", "profile", "email", "offline_access"} 6 } 7 8 authorizationUrl, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options) 9 c.Redirect(http.StatusFound, authorizationUrl.String()) 10 }) ``` * Java AuthController.java ```java 4 collapsed lines 1 import org.springframework.web.bind.annotation.GetMapping; 2 import org.springframework.web.bind.annotation.RestController; 3 import java.net.URL; 4 5 // Handle indirect auth entry points 6 @GetMapping("/login") 7 public String login() { 8 String redirectUri = "http://localhost:3000/auth/callback"; 9 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 10 options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access")); 11 12 URL authorizationUrl = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options); 13 return "redirect:" + authorizationUrl.toString(); 14 } ``` --- # DOCUMENT BOUNDARY --- # Redirects > 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. --- # DOCUMENT BOUNDARY --- # Personalize email delivery > Learn how to personalize email delivery by using Scalekit's managed service or configuring your own SMTP provider for brand consistency and control. Email delivery is a critical part of your authentication flow. By default, Scalekit sends all authentication emails (sign-in verification, sign-up confirmation, password reset) through its own email service. However, for production applications, you may need more control over email branding, deliverability, and compliance requirements. Here are common scenarios where you’ll want to customize email delivery: * **Brand consistency**: Send emails from your company’s domain with your own sender name and email address to maintain brand trust * **Deliverability optimization**: Use your established email reputation and delivery infrastructure to improve inbox placement * **Compliance requirements**: Meet specific regulatory or organizational requirements for email handling and data sovereignty * **Email analytics**: Track email metrics and performance through your existing email service provider * **Custom domains**: Ensure emails come from your verified domain to avoid spam filters and build user trust * **Enterprise requirements**: Corporate customers may require emails to come from verified business domains Scalekit provides two approaches to handle email delivery, allowing you to choose the right balance between simplicity and control. ![Email delivery methods in Scalekit](/.netlify/images?url=_astro%2F1-email-delivery-method.efqY1l72.png\&w=2848\&h=1720\&dpl=69cce21a4f77360008b1503a) ## Use Scalekit’s managed email service Default [Section titled “Use Scalekit’s managed email service ”](#use-scalekits-managed-email-service) The simplest approach requires no configuration. Scalekit handles all email delivery using its own infrastructure. **When to use this approach:** * Quick setup for development and testing * You don’t need custom branding * You want Scalekit to handle email deliverability **Default settings:** * **Sender Name**: Team workspace\_name * **From Email Address**: * **Infrastructure**: Fully managed by Scalekit No additional configuration is required. Your authentication emails will be sent automatically with these settings. Tip You can customize the sender name in your dashboard settings while still using Scalekit’s email infrastructure. ## Configure your own email provider [Section titled “Configure your own email provider”](#configure-your-own-email-provider) For production applications, you’ll likely want to use your own email provider to maintain brand consistency and control deliverability. When to use this approach: * You need emails sent from your domain * You want complete control over email deliverability * You need to meet compliance requirements (e.g. GDPR, CCPA) * You want to integrate with existing email analytics ### Gather your SMTP credentials [Section titled “Gather your SMTP credentials”](#gather-your-smtp-credentials) Before configuring, collect the following information 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 authentication password | | **Sender Email** | The email address emails will be sent from | | **Sender Name** | The display name recipients will see | ### Configure SMTP settings in Scalekit [Section titled “Configure SMTP settings in Scalekit”](#configure-smtp-settings-in-scalekit) 1. Navigate to email settings In your Scalekit dashboard, go to **Emails**. 2. Select custom email provider Choose **Use your own email provider** from the email delivery options 3. Configure sender information ```plaintext 1 From Email Address: noreply@yourdomain.com 2 Sender Name: Your Company Name ``` 4. Enter SMTP configuration ```plaintext 1 SMTP Server Host: smtp.your-provider.com 2 SMTP Port: 587 3 SMTP Username: your-username 4 SMTP Password: your-password ``` 5. Save and test configuration Click **Save** to apply your settings, then send a test email to verify the configuration ### Common provider configurations [Section titled “Common provider configurations”](#common-provider-configurations) * SendGrid ```plaintext 1 Host: smtp.sendgrid.net 2 Port: 587 3 Username: apikey 4 Password: [Your SendGrid API Key] ``` * Amazon SES ```plaintext 1 Host: email-smtp.us-east-1.amazonaws.com 2 Port: 587 3 Username: [Your SMTP Username from AWS] 4 Password: [Your SMTP Password from AWS] ``` * Postmark ```plaintext 1 Host: smtp.postmarkapp.com 2 Port: 587 3 Username: [Your Postmark Server Token] 4 Password: [Your Postmark Server Token] ``` Note All SMTP credentials are encrypted and stored securely. Email transmission uses TLS encryption for security. ## Test your email configuration [Section titled “Test your email configuration”](#test-your-email-configuration) After configuring your email provider, verify that everything works correctly: 1. Send a test email through your authentication flow 2. Check delivery to ensure emails reach the intended recipients 3. Verify sender information appears correctly in the recipient’s inbox 4. Confirm formatting, branding, links and buttons work as expected --- # DOCUMENT BOUNDARY --- # Managing organization identifiers & metadata > Learn how to use external IDs and metadata to manage and track organizations in Scalekit, associating your own identifiers and storing custom key-value pairs. Applications often need to manage and track resources in their own systems. Scalekit provides two features to help with this: * **External IDs**: Associate your own identifiers with organizations * **Metadata**: Store custom key-value pairs with organizations ### When to use external IDs and metadata [Section titled “When to use external IDs and metadata”](#when-to-use-external-ids-and-metadata) Use these features when you need to: * Track organizations using your own identifiers instead of Scalekit’s IDs * Store additional information about organizations like billing details or internal codes * Integrate Scalekit organizations with your existing systems ### Add an external ID to an organization [Section titled “Add an external ID to an organization”](#add-an-external-id-to-an-organization) External IDs let you identify organizations using your own identifiers. You can set an external ID when creating or updating an organization. #### Create a new organization with an external ID [Section titled “Create a new organization with an external ID”](#create-a-new-organization-with-an-external-id) This example shows how to create an organization with your custom identifier: Create a new organization with an external ID ```bash 1 curl https:///api/v1/organizations \ 2 --request POST \ 3 --header 'Content-Type: application/json' \ 4 --data '{ 5 "display_name": "Megasoft Inc", 6 "external_id": "CUST-12345-MGSFT", 7 }' ``` #### Update an existing organization’s external ID [Section titled “Update an existing organization’s external ID”](#update-an-existing-organizations-external-id) To change an organization’s external ID, use the update endpoint: Update an existing organization's external ID ```bash 1 curl 'https:///api/v1/organizations/{id}' \ 2 --request PATCH \ 3 --header 'Content-Type: application/json' \ 4 --data '{ 5 "display_name": "Megasoft Inc", 6 "external_id": "TENANT-12345-MGSFT", 7 }' ``` ### Add metadata to an organization [Section titled “Add metadata to an organization”](#add-metadata-to-an-organization) Metadata lets you store custom information as key-value pairs. You can add metadata when creating or updating an organization. #### Create a new organization with metadata [Section titled “Create a new organization with metadata”](#create-a-new-organization-with-metadata) This example shows how to store billing information with a new organization: Create a new organization with metadata ```bash 1 curl https:///api/v1/organizations \ 2 --request POST \ 3 --header 'Content-Type: application/json' \ 4 --data '{ 5 "display_name": "Megasoft Inc", 6 "metadata": { 7 "invoice_email": "invoices@megasoft.com" 8 } 9 }' ``` #### Update an existing organization’s metadata [Section titled “Update an existing organization’s metadata”](#update-an-existing-organizations-metadata) To modify an organization’s metadata, use the update endpoint: Update an existing organization's metadata ```bash 1 curl 'https:///api/v1/organizations/{id}' \ 2 --request PATCH \ 3 --header 'Content-Type: application/json' \ 4 --data '{ 5 "display_name": "Megasoft Inc", 6 "metadata": { 7 "invoice_email": "billing@megasoft.com" 8 } 9 }' ``` ### View external IDs and metadata [Section titled “View external IDs and metadata”](#view-external-ids-and-metadata) All organization endpoints that return organization details will include the external ID and metadata in their responses. This makes it easy to access your custom data when working with organizations. --- # DOCUMENT BOUNDARY --- # ID token claims > Inspect the contents of the ID token An ID token is a JSON Web Token (JWT) containing cryptographically signed claims about a user’s profile information. Scalekit issues this token after successful authentication. The ID token is a Base64-encoded JSON object with three parts: header, payload, and signature. Here’s an example of the payload. Note this is formatted for readability and the header and signature fields are skipped. Sample IdToken payload ```json 1 { 2 "iss": "https://yoursaas.scalekit.com", 3 "azp": "skc_12205605011849527", 4 "aud": ["skc_12205605011849527"], 5 "amr": ["conn_17576372041941092"], 6 "sub": "conn_17576372041941092;google-oauth2|104630259163176101050", 7 "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q", 8 "c_hash": "HK6E_P6Dh8Y93mRNtsDB1Q", 9 "iat": 1353601026, 10 "exp": 1353604926, 11 "name": "John Doe", 12 "given_name": "John", 13 "family_name": "Doe", 14 "picture": "https://lh3.googleusercontent.com/a/ACg8ocKNE4TZj2kyLOj094kie_gDlUyU7JCZtbaiEma17URCEf=s96-c", 15 "locale": "en", 16 "email": "john.doe@acmecorp.com", 17 "email_verified": true 18 } ``` ## Full list of ID token claims [Section titled “Full list of ID token claims”](#full-list-of-id-token-claims) | Claim | Presence | Description | | ---------------- | -------- | -------------------------------------------- | | `aud` | Always | Intended audience (client ID) | | `amr` | Always | Authentication method reference values | | `exp` | Always | Expiration time (Unix timestamp) | | `iat` | Always | Issuance time (Unix timestamp) | | `iss` | Always | Issuer identifier (Scalekit environment URL) | | `oid` | Always | Organization ID of the user | | `sub` | Always | Subject identifier for the user | | `at_hash` | Always | Access token hash | | `c_hash` | Always | Authorization code hash | | `azp` | Always | Authorized presenter (usually same as `aud`) | | `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 | ## Verifying the ID token [Section titled “Verifying the ID token”](#verifying-the-id-token) In some cases, you may need to parse the ID token manually—for example, to access custom claims that are not part of the standard `User` object in the SDK method. These details are encoded in the ID token as JSON Web Token (JWT). If you use the Scalekit SDK, token validation is handled automatically. For non-SDK integrations (e.g., Ruby, PHP, or other languages), follow the steps below. ### Key validation parameters [Section titled “Key validation parameters”](#key-validation-parameters) | Parameter | Value | | -------------------- | -------------------------------------------------------------------- | | Signing algorithm | `RS256` | | JWKS endpoint | `https:///keys` | | Issuer (`iss`) | Your Scalekit environment URL (e.g., `https://yourapp.scalekit.com`) | | OpenID configuration | `https:///.well-known/openid-configuration` | ### Manual validation steps [Section titled “Manual validation steps”](#manual-validation-steps) To verify the signature manually: 1. Fetch the OpenID configuration from `https:///.well-known/openid-configuration` to discover `issuer` and `jwks_uri`. 2. Fetch the public signing keys from the `jwks_uri` (e.g., `https:///keys`). 3. Use a JWT library for your language to decode and verify the token with `RS256` using those keys. 4. Validate the required claims listed below. ### Important claims [Section titled “Important claims”](#important-claims) When validating, pay attention to these claims: * **`iss` (Issuer)**: This must match your Scalekit environment URL. * **`aud` (Audience)**: This must match your application’s client ID. * **`exp` (Expiration Time)**: Ensure the token has not expired. * **`sub` (Subject)**: This uniquely identifies the user, often combining the `connection_id` and the identity provider’s unique user ID. * **`amr`**: Contains the `connection_id` used for authentication. This structure provides a neutral, factual reference for ID token claims in Scalekit, organized according to the data structure itself. An ID token is a cryptographically signed Base64-encoded JSON object containing name/value pairs about the user’s profile information. It is a JWT token. Validate an ID token before using it. Since you communicate directly with Scalekit over HTTPS and use your client secret to exchange the `code` for the ID token, you can be confident that the token comes from Scalekit and is valid. If you use the Scalekit SDK to exchange the code for the ID token, the SDK automatically decodes the base64url-encoded values, parses the JSON, validates the JWT, and accesses the claims within the ID token. --- # 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 --- # Agent connectors > Connect AI applications to tools and data from Slack, Google Workspace, Salesforce, and more. Agent connectors enable AI-powered applications to connect to tools and data from popular platforms such as Slack, Google Workspace, Salesforce, Notion, and more. Each connector provides OAuth or API key authentication and exposes tools your agents can use. Choose a connector below to view setup instructions and available tools: [Airtable ](/reference/agent-connectors/airtable/)Connect to Airtable. Manage databases, tables, records, and collaborate on structured data [Apollo ](/reference/agent-connectors/apollo/)Connect to Apollo.io to search and enrich B2B contacts and accounts, manage CRM contacts, and automate outreach sequences. [Asana ](/reference/agent-connectors/asana/)Connect to Asana. Manage tasks, projects, teams, and workflow automation [Attention ](/reference/agent-connectors/attention/)Connect to Attention for AI insights, conversations, teams, and workflows [Attio ](/reference/agent-connectors/attio/)Connect to Attio CRM to manage contacts, companies, deals, notes, tasks, and lists with a modern relationship management platform. [Brave Search ](/reference/agent-connectors/brave-search/)Connect to Brave Search to run privacy-first web, news, and local searches, retrieve AI summaries, get LLM grounding context, and use search-backed chat completions. [Chorus ](/reference/agent-connectors/chorus/)Connect to Chorus.ai to sync calls, transcripts, conversation intelligence, and analytics. [Clari Copilot ](/reference/agent-connectors/clari_copilot/)Connect to Clari Copilot for sales call transcripts, analytics, call data, and insights. [ClickUp ](/reference/agent-connectors/clickup/)Connect to ClickUp. Manage tasks, projects, workspaces, and team collaboration [Confluence ](/reference/agent-connectors/confluence/)Connect to Confluence. Manage spaces, pages, content, and team collaboration [Discord ](/reference/agent-connectors/discord/)Connect to Discord. Read user profiles, list guilds, retrieve member data, check entitlements, and verify OAuth2 authorization details. [Dropbox ](/reference/agent-connectors/dropbox/)Connect to Dropbox. Manage files, folders, sharing, and cloud storage workflows [evertrace.ai ](/reference/agent-connectors/evertrace/)Connect to evertrace.ai to search and manage talent signals, saved searches, and lists [Exa ](/reference/agent-connectors/exa/)Connect to Exa for AI-powered semantic web search, content enrichment, finding similar pages, website crawling, direct answers, structured research, and large-scale URL discovery. [Fathom ](/reference/agent-connectors/fathom/)Connect to Fathom AI meeting assistant. Record, transcribe, and summarize meetings with AI-powered insights [Figma ](/reference/agent-connectors/figma/)Connect to Figma API v1. Read and write files, manage components, styles, variables, webhooks, and dev resources. Enterprise features include library analytics and activity logs. [Freshdesk ](/reference/agent-connectors/freshdesk/)Connect to Freshdesk. Manage tickets, contacts, companies, and customer support workflows [Github ](/reference/agent-connectors/github/)GitHub is a cloud-based Git repository hosting service that allows developers to store, manage, and track changes to their code. [GitLab ](/reference/agent-connectors/gitlab/)Connect to GitLab to manage repositories, issues, merge requests, CI/CD pipelines, groups, releases, and more with 110 tools. [Gmail ](/reference/agent-connectors/gmail/)Gmail is Google's cloud based email service that allows you to access your messages from any computer or device with just a web browser. [Gong ](/reference/agent-connectors/gong/)Connect with Gong to sync calls, transcripts, insights, coaching and CRM activity [Google Ads ](/reference/agent-connectors/google_ads/)Connect to Google Ads to manage advertising campaigns, analyze performance metrics, and optimize ad spending across Google's advertising platform [Google BigQuery ](/reference/agent-connectors/bigquery/)BigQuery is Google Cloud’s fully-managed enterprise data warehouse for analytics at scale. [Google Calendar ](/reference/agent-connectors/googlecalendar/)Google Calendar is Google's cloud-based calendar service that allows you to manage your events, appointments, and schedules from any computer or device with just a web browser. [Google Docs ](/reference/agent-connectors/googledocs/)Connect to Google Docs. Create, edit, and collaborate on documents [Google Drive ](/reference/agent-connectors/googledrive/)Connect to Google Drive. Manage files, folders, and sharing permissions [Google Forms ](/reference/agent-connectors/googleforms/)Connect to Google Forms. Create, view, and manage forms and responses securely [Google Meet ](/reference/agent-connectors/googlemeet/)Connect to Google Meet. Create and manage video meetings with powerful collaboration features [Google Sheets ](/reference/agent-connectors/googlesheets/)Connect to Google Sheets. Create, edit, and analyze spreadsheets with powerful data management capabilities [Google Slides ](/reference/agent-connectors/googleslides/)Connect to Google Slides to create, read, and modify presentations programmatically. [Granola MCP ](/reference/agent-connectors/granolamcp/)Connect to Granola MCP to search meeting notes, inspect meeting details, and retrieve transcripts from Granola's official MCP server. [HarvestAPI ](/reference/agent-connectors/harvestapi/)Connect to HarvestAPI to log time in Harvest and access LinkedIn data — profiles, companies, posts, ads, jobs, and connection management. [HubSpot ](/reference/agent-connectors/hubspot/)Connect to HubSpot CRM. Manage contacts, deals, companies, and marketing automation [Intercom ](/reference/agent-connectors/intercom/)Connect to Intercom. Send messages, manage conversations, and interact with users and contacts. [Jira ](/reference/agent-connectors/jira/)Connect to Jira. Manage issues, projects, workflows, and agile development processes [Linear ](/reference/agent-connectors/linear/)Connect to Linear. Manage issues, projects, sprints, and development workflows [Microsoft Excel ](/reference/agent-connectors/microsoftexcel/)Connect to Microsoft Excel. Access, read, and modify spreadsheets stored in OneDrive or SharePoint through Microsoft Graph API. [Microsoft Word ](/reference/agent-connectors/microsoftword/)Connect to Microsoft Word. Authenticate with your Microsoft account to create, read, and edit Word documents stored in OneDrive or SharePoint through Microsoft Graph API. [Monday.com ](/reference/agent-connectors/monday/)Connect to Monday.com. Manage boards, tasks, workflows, teams, and project collaboration [Notion ](/reference/agent-connectors/notion/)Connect to Notion workspace. Create, edit pages, manage databases, and collaborate on content [OneDrive ](/reference/agent-connectors/onedrive/)Connect to OneDrive. Manage files, folders, and cloud storage with Microsoft OneDrive [OneNote ](/reference/agent-connectors/onenote/)Connect to Microsoft OneNote. Access, create, and manage notebooks, sections, and pages stored in OneDrive or SharePoint through Microsoft Graph API. [Outlook ](/reference/agent-connectors/outlook/)Connect to Microsoft Outlook. Manage emails, calendar events, contacts, and tasks [PhantomBuster ](/reference/agent-connectors/phantombuster/)Connect to PhantomBuster. Launch and manage automation agents, stream container output, manage leads and lead lists, run AI completions, export usage reports, and control your entire organization programmatically. [Pipedrive ](/reference/agent-connectors/pipedrive/)Connect to Pipedrive CRM. Manage deals, persons, organizations, leads, activities, and sales pipelines with 68 tools. [Salesforce ](/reference/agent-connectors/salesforce/)Connect to Salesforce CRM. Manage leads, opportunities, accounts, and customer relationships [ServiceNow ](/reference/agent-connectors/servicenow/)Connect to ServiceNow. Manage incidents, service requests, CMDB, and IT service management workflows [SharePoint ](/reference/agent-connectors/sharepoint/)Connect to SharePoint. Manage sites, documents, lists, and collaborative content [Slack ](/reference/agent-connectors/slack/)Connect to Slack workspace. Send Messages as Bots or on behalf of users [Snowflake ](/reference/agent-connectors/snowflake/)Connect to Snowflake to manage and analyze your data warehouse workloads [Snowflake Key Pair Auth ](/reference/agent-connectors/snowflakekeyauth/)Connect to Snowflake via Public Private Key Pair to manage and analyze your data warehouse workloads [Teams ](/reference/agent-connectors/microsoftteams/)Connect to Microsoft Teams. Manage messages, channels, meetings, and team collaboration [Trello ](/reference/agent-connectors/trello/)Connect to Trello. Manage boards, cards, lists, and team collaboration workflows [Vimeo ](/reference/agent-connectors/vimeo/)Connect to Vimeo API v3.4. Upload and manage videos, organize content into showcases and folders, manage channels, handle comments, likes, and webhooks. [YouTube ](/reference/agent-connectors/youtube/)Connect to YouTube to access channel details, analytics, and upload or manage videos via OAuth 2.0 [Zendesk ](/reference/agent-connectors/zendesk/)Connect to Zendesk. Manage customer support tickets, users, organizations, and help desk operations [Zoom ](/reference/agent-connectors/zoom/)Connect to Zoom. Schedule meetings, manage recordings, and handle video conferencing workflows --- # 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) **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). ![](/.netlify/images?url=_astro%2F0.vqDHIV-X.png\&w=3270\&h=954\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) Generate an authorization URL with two additional parameters— `identity_provider` and `login_hint` — to redirect users seamlessly: Example Code ```typescript 1 import { Issuer, Client } from "openid-client"; 2 3 const client = await getOidcClient(); 4 5 const authUrl = client.authorizationUrl({ 6 scope: "openid email", 7 state: state, 8 nonce: nonce, 9 identity_provider: "ScalekitIdPRouter", // Same as Provider name (above) 10 login_hint: email, // User's company email address 11 }); 12 console.log("authUrl", authUrl); 13 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 1 https://[domain].auth.[region].amazoncognito.com/oauth2/authorize 2 ?client_id=k6tana1l8b0bvhk9gfixkurr6 3 &scope=openid%20email 4 &response_type=code 5 &redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback 6 &state=-5iLRZmPwwdqwqT-A4yiJM6KQvCLQM0JRx9QaXOlzRE 7 &nonce=sGSXePnJ0Ue5GZyTpKG4rRsVeWyfZloImbMWunUDbG4 8 &identity_provider=ScalekitIdPRouter 9 &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=69cce21a4f77360008b1503a) 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](/guides/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=69cce21a4f77360008b1503a) ## 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 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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 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 1 npm install @scalekit-sdk/node firebase-admin ``` backend/server.js ```javascript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import admin from 'firebase-admin'; 3 4 // Initialize Scalekit 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 // 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 --- # Authenticate customer apps > Use Scalekit to implement OAuth for customer apps. Issue tokens and validate API requests with JWKS This guide explains how you enable API authentication for your customers’ applications using Scalekit’s OAuth 2.0 client credentials flow. When your customers build applications that need to access your API, they use client credentials registered through your Scalekit environment to obtain access tokens. Your API validates these tokens to authorize their requests using JWKS. ## How your customers’ applications authenticate with your API [Section titled “How your customers’ applications authenticate with your API”](#how-your-customers-applications-authenticate-with-your-api) Your Scalekit environment functions as an OAuth 2.0 Authorization Server. Your customers’ applications authenticate using the client credentials flow, exchanging their registered client ID and secret for access tokens that authorize API requests to your platform. ### Storing client credentials [Section titled “Storing client credentials”](#storing-client-credentials) Your customers’ applications securely store the credentials you issued to them in environment variables. This example shows how their applications would store these credentials: Environment variables in customer's application ```sh 1 YOURAPP_ENVIRONMENT_URL="" 2 YOURAPP_CLIENT_ID="" 3 YOURAPP_CLIENT_SECRET="" ``` These credentials are obtained when you register an API client for your customer (see the [quickstart guide](/authenticate/m2m/api-auth-quickstart/) for client registration). ### Obtaining access tokens [Section titled “Obtaining access tokens”](#obtaining-access-tokens) Your customers’ applications obtain access tokens from your Scalekit authorization server before making API requests. They send their credentials to your token endpoint: Token endpoint ```sh 1 https:///oauth/token ``` Here’s how your customers’ applications request access tokens: * cURL ```sh 1 curl -X POST \ 2 "https:///oauth/token" \ 3 -H "Content-Type: application/x-www-form-urlencoded" \ 4 -d "grant_type=client_credentials" \ 5 -d "client_id=" \ 6 -d "client_secret=" \ 7 -d "scope=openid profile email" ``` * Python ```python 1 import os 2 import json 3 import requests 4 5 # Customer's application configuration 6 env_url = os.environ['YOURAPP_SCALEKIT_ENVIRONMENT_URL'] 7 8 def get_m2m_access_token(): 9 """ 10 Customer's application requests an access token using client credentials. 11 This token will be used to authenticate API requests to your platform. 12 """ 13 headers = {"Content-Type": "application/x-www-form-urlencoded"} 14 params = { 15 "grant_type": "client_credentials", 16 "client_id": os.environ['YOURAPP_SCALEKIT_CLIENT_ID'], 17 "client_secret": os.environ['YOURAPP_SCALEKIT_CLIENT_SECRET'], 18 "scope": "openid profile email" 19 } 20 21 response = requests.post( 22 url=f"{env_url}/oauth/token", 23 headers=headers, 24 data=params, 25 verify=True 26 ) 27 28 access_token = response.json().get('access_token') 29 return access_token ``` Your authorization server returns a JSON response containing the access token: Token response ```json 1 { 2 "access_token": "", 3 "token_type": "Bearer", 4 "expires_in": 86399, 5 "scope": "openid" 6 } ``` | Field | Description | | -------------- | ----------------------------------------------------- | | `access_token` | Token for authenticating API requests | | `token_type` | Always “Bearer” for this flow | | `expires_in` | Token validity period in seconds (typically 24 hours) | | `scope` | Authorized scopes for this token | ### Using access tokens [Section titled “Using access tokens”](#using-access-tokens) After obtaining an access token, your customers’ applications include it in the Authorization header when making requests to your API: Customer's application making an API request ```sh 1 curl --request GET "https://" \ 2 -H "Content-Type: application/json" \ 3 -H "Authorization: Bearer " ``` ## Validating access tokens [Section titled “Validating access tokens”](#validating-access-tokens) Your API server must validate access tokens before processing requests. Scalekit uses JSON Web Tokens (JWTs) signed with RSA keys, which you validate using the JSON Web Key Set (JWKS) endpoint. ### Retrieving JWKS [Section titled “Retrieving JWKS”](#retrieving-jwks) Your application should fetch the public keys from the JWKS endpoint: JWKS endpoint ```sh 1 https:///keys ``` JWKS response ```json 1 { 2 "keys": [ 3 { 4 "use": "sig", 5 "kty": "RSA", 6 "kid": "snk_58327480989122566", 7 "alg": "RS256", 8 "n": "wUaqIj3pIE_zfGN9u4GySZs862F-0Kl-..", 9 "e": "AQAB" 10 } 11 ] 12 } ``` ### Token validation process [Section titled “Token validation process”](#token-validation-process) When your API receives a request with a JWT, follow these steps: 1. Extract the token from the Authorization header 2. Fetch the JWKS from the endpoint 3. Use the public key from JWKS to verify the token’s signature 4. Validate the token’s claims (issuer, audience, expiration) This example shows how to fetch JWKS data: Fetch JWKS with cURL ```sh 1 curl -s "https:///keys" | jq ``` * jwksClient (Node.js) Express.js ```javascript 1 const express = require('express'); 2 const jwt = require('jsonwebtoken'); 3 const jwksClient = require('jwks-rsa'); 4 const app = express(); 5 6 // Initialize JWKS client to validate tokens from customer applications 7 // This fetches public keys from your Scalekit environment 8 const client = jwksClient({ 9 jwksUri: `https:///keys` 10 }); 11 12 // Function to get signing key for token verification 13 function getKey(header, callback) { 14 client.getSigningKey(header.kid, function(err, key) { 15 if (err) return callback(err); 16 17 const signingKey = key.publicKey || key.rsaPublicKey; 18 callback(null, signingKey); 19 }); 20 } 21 22 // Middleware to validate JWT from customer's API client application 23 function validateJwt(req, res, next) { 24 // Extract token sent by customer's application 25 const authHeader = req.headers.authorization; 26 if (!authHeader || !authHeader.startsWith('Bearer ')) { 27 return res.status(401).json({ error: 'Missing authorization token' }); 28 } 29 30 const token = authHeader.split(' ')[1]; 31 32 // Verify the token signature using JWKS 33 jwt.verify(token, getKey, { 34 algorithms: ['RS256'] 35 }, (err, decoded) => { 36 if (err) { 37 return res.status(401).json({ error: 'Invalid token', details: err.message }); 38 } 39 40 // Token is valid - add decoded claims to request 41 req.user = decoded; 42 next(); 43 }); 44 } 45 46 // Apply validation middleware to your API routes 47 app.use('/api', validateJwt); 48 49 // Example protected API endpoint 50 app.get('/api/data', (req, res) => { 51 res.json({ 52 message: 'Customer application authenticated successfully', 53 userId: req.user.sub 54 }); 55 }); 56 57 app.listen(3000, () => { 58 console.log('API server running on port 3000'); 59 }); ``` * Python Flask ```python 9 collapsed lines 1 from scalekit import ScalekitClient 2 import os 3 4 # Initialize Scalekit SDK to validate tokens from customer applications 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 def validate_api_request(request): 12 """ 13 Validate access token from customer's API client application. 14 Your API uses this to authorize requests from customer applications. 15 """ 16 # Extract token sent by customer's application 17 auth_header = request.headers.get('Authorization') 18 if not auth_header or not auth_header.startswith('Bearer '): 19 return None, "Missing authorization token" 20 21 token = auth_header.split(' ')[1] 22 23 try: 24 # Validate token and extract claims using Scalekit SDK 25 claims = scalekit_client.validate_access_token_and_get_claims( 26 token=token 27 ) 28 29 # Token is valid - return claims for authorization logic 30 return claims, None 31 except Exception as e: 32 return None, f"Invalid token: {str(e)}" 33 34 # Example: Use in your Flask API endpoint 35 @app.route('/api/data', methods=['GET']) 36 def get_data(): 37 claims, error = validate_api_request(request) 38 39 if error: 40 return {"error": error}, 401 41 42 # Customer application is authenticated 43 return { 44 "message": "Customer application authenticated successfully", 45 "userId": claims.get("sub") 46 } ``` Token validation best practices When implementing token validation in your API: 1. Always verify the token signature using the public key from JWKS 2. Validate token expiration and required claims (issuer, audience, expiration) 3. Cache JWKS responses to improve performance and reduce latency 4. Implement token revocation checks for sensitive operations 5. Use HTTPS for all API endpoints to prevent token interception 6. Check scopes in the token claims to enforce fine-grained permissions ### SDK support status [Section titled “SDK support status”](#sdk-support-status) All Scalekit SDKs include helpers for validating access tokens: * **Node.js**: Provides `validateAccessToken` and `validateToken` methods with `TokenValidationOptions` for validating issuer, audience, and required scopes. * **Python**: Provides `validate_access_token`, `validate_token`, and `validate_access_token_and_get_claims` methods with `TokenValidationOptions` for validating issuer, audience, and required scopes. * **Go**: Provides `ValidateAccessToken`, generic `ValidateToken[T]`, and `GetAccessTokenClaims` helpers that validate tokens using JWKS and return typed claims with errors. These methods accept `context.Context` as the first argument for cancellation and timeout. * **Java**: Provides `validateAccessToken` (boolean) and `validateAccessTokenAndGetClaims` (returns claims and throws `APIException`) for token validation in JVM applications. You can still use standard JWT libraries with the JWKS endpoint, as shown in the examples above, when you need custom validation logic or cannot use an SDK in your API service. --- # DOCUMENT BOUNDARY --- # Machine-2-Machine authentication > Secure interactions between software systems with M2M authentication, enabling secure API access for AI agents, apps, and automated workflows Machine-2-Machine (M2M) authentication secures API access for non-human clients like AI agents, third-party integrations, backend services, and automated workflows. When you need to give these machine clients secure access to your APIs, M2M authentication provides credential-based authentication using client IDs and secrets, without exposing hardcoded tokens or requiring human interaction. Your machine clients can act on behalf of an organization, a specific user, or operate independently to perform system-level tasks. You get centralized management of all machine identities with granular permissions and seamless credential rotation across internal and external services. This approach ensures your machine clients authenticate with the same rigour as human users, giving you secure, scoped access to APIs while simplifying integration development and meeting enterprise security standards. ## When to use M2M authentication [Section titled “When to use M2M authentication”](#when-to-use-m2m-authentication) You’ll use M2M auth when your APIs need to be accessed by: * Automated clients or AI agents making requests on behalf of users or organizations * External platforms or third-party integrations (like Zapier, CRM systems, analytics platforms, or payment providers) * Internal services or background jobs that programmatically invoke your APIs * Scheduled services that automatically sync data with your API * Automated workflows that update external systems In all these cases, there’s no human user session involved. The system still needs a secure way to authenticate the client and determine what access it should have. ## Understanding the OAuth 2.0 client credentials flow [Section titled “Understanding the OAuth 2.0 client credentials flow”](#understanding-the-oauth-20-client-credentials-flow) M2M authentication uses the OAuth 2.0 client credentials flow. This is the standard way for non-human clients to obtain access tokens without requiring user interaction. OAuth 2.0 is an authorization framework that allows client applications to access protected resources on a resource server by presenting an access token. The protocol delegates authorization decisions to a central authorization server, which issues access tokens after validating the client or user. The protocol defines several grant types for different use cases: * **Client credentials flow** - Use this when one system (like an automated client or AI agent) wants to access another system’s API * **Authorization code flow** - Use this when a user authorizes a machine client to act on their behalf For org-level or internal service clients, you use a `client_id` and `client_secret` to authenticate. For user-backed clients, the user first authorizes the client via the authorization code flow. ## Choose your client type [Section titled “Choose your client type”](#choose-your-client-type) Scalekit provides three types of machine clients based on the OAuth 2.0 flow: * **Org-level clients:** Use these when your automated client needs to access APIs on behalf of an organization. Tokens are scoped to a specific org (`oid`) and work well for org-wide workflows. Read the M2M authentication quickstart to set up an org-level client. * **User-level clients:** Use these when your machine client acts on behalf of a specific user. These tokens include a `uid` (user ID) in addition to `oid`, letting you enforce user-contextual access. *(Coming soon)* * **Internal service clients:** Use these for secure service-to-service communication between internal systems. These clients issue tokens with an `aud` (audience) claim to enforce destination-specific access. They’re ideal for microservices that need to communicate without org or user context. *(Coming soon)* ![How M2M authentication works](/.netlify/images?url=_astro%2Fm2m-flow.Bl90F1XY.png\&w=4140\&h=3564\&dpl=69cce21a4f77360008b1503a) ## How the authentication flow works [Section titled “How the authentication flow works”](#how-the-authentication-flow-works) Here’s the complete M2M authentication flow: 1. **Register a machine client** You create an M2M client in Scalekit for the machine that needs access to your APIs. 2. **Generate credentials** Scalekit issues a `client_id` and `client_secret` for that client. Your client uses these credentials to request access tokens. 3. **Request an access token** Your client requests an access token from Scalekit’s `/oauth/token` endpoint. For org-level access, it uses the client credentials flow directly. For user-level access, it exchanges an authorization code after user consent in the authorization code flow. 4. **Receive a signed JWT** Scalekit validates the request and returns a short-lived, signed JWT that contains claims specific to your client type: * Which organization it belongs to (`oid`) * Which user it belongs to (`uid`) * What it’s allowed to do (`scopes`) * How long it’s valid for (`exp`, `nbf`) * Which service it’s intended for (`aud`) Each token is signed by Scalekit so your API can validate it locally without calling back to Scalekit. This improves performance and keeps your authorization flow resilient even if the auth server is briefly unavailable. 5. **Make authenticated API calls** Your machine client sends this token in the `Authorization` header when calling your API. 6. **Validate the token** Your API checks the token’s signature and claims locally. You don’t need to make a network call to Scalekit for validation. This approach gives you secure, programmatic authentication using short-lived, scoped tokens that you can revoke or rotate as needed. ## What Scalekit handles for you [Section titled “What Scalekit handles for you”](#what-scalekit-handles-for-you) Building secure M2M authentication from scratch can be complex when dealing with token scoping, TTL management, credential rotation, and validation. Scalekit handles these concerns out of the box with minimal setup. With just a few API calls or dashboard actions, you can: * Register machine clients scoped to an organization, user, or service * Generate and manage credentials with safe rotation * Issue signed, short-lived JWTs with the right claims (`oid`, `uid`, `aud`, `scopes`) based on the client type * Validate tokens locally in your API without calling back to Scalekit You can enforce least-privilege access for machine clients without implementing the OAuth flow or token lifecycle yourself. ## Token security and management [Section titled “Token security and management”](#token-security-and-management) Tip Tokens issued by Scalekit are designed to be secure by default and operationally smooth to manage over time: * **Short-lived**: All tokens have a configurable TTL (default: 1 hour; minimum: 5 minutes) to reduce long-term risk. * **Locally verifiable**: Tokens are signed JWTs that your API can verify without calling back to Scalekit. * **Supports rotation**: Each client can store up to five secrets at a time, making credential rotation seamless with no downtime. * **Includes identity context**: Tokens contain claims like `oid` (org ID), `uid` (user ID), and `aud` (audience) so you can enforce precise access. * **Scoped access**: You define fine-grained scopes to limit what each client is allowed to do. These defaults ensure that your tokens are short-lived, constrained in what they can do, and fully verifiable without external dependencies. ## Key benefits [Section titled “Key benefits”](#key-benefits) When you implement M2M authentication with Scalekit, you get: * **Security**: You eliminate the need to share user credentials between services or expose hardcoded secrets * **Auditability**: Each service has its own identity, making it easier for you to track and audit API usage * **Scalability**: You can easily add or remove services without affecting other parts of your system * **Granular Control**: You can implement fine-grained access control at the service level To start integrating M2M authentication in your application, head to the [quickstart guide](/authenticate/m2m/api-auth-quickstart) for setting up an org-level client. --- # DOCUMENT BOUNDARY --- # Bring your own email provider > Scalekit allows you to configure your own email provider to improve deliverability and security. Email delivery is a critical part of your authentication flow. By default, Scalekit sends all authentication emails (sign-in verification, sign-up confirmation, password reset) through its own email service. However, for production applications, you may need more control over email branding, deliverability, and compliance requirements. Here are common scenarios where you’ll want to customize email delivery: * **Brand consistency**: Send emails from your company’s domain with your own sender name and email address to maintain brand trust * **Deliverability optimization**: Use your established email reputation and delivery infrastructure to improve inbox placement * **Compliance requirements**: Meet specific regulatory or organizational requirements for email handling and data sovereignty * **Email analytics**: Track email metrics and performance through your existing email service provider * **Custom domains**: Ensure emails come from your verified domain to avoid spam filters and build user trust * **Enterprise requirements**: Corporate customers may require emails to come from verified business domains Scalekit provides two approaches to handle email delivery, allowing you to choose the right balance between simplicity and control. ![Email delivery methods in Scalekit](/.netlify/images?url=_astro%2F1-email-delivery-method.efqY1l72.png\&w=2848\&h=1720\&dpl=69cce21a4f77360008b1503a) ## Use Scalekit’s managed email service Default [Section titled “Use Scalekit’s managed email service ”](#use-scalekits-managed-email-service) The simplest approach requires no configuration. Scalekit handles all email delivery using its own infrastructure. **When to use this approach:** * Quick setup for development and testing * You don’t need custom branding * You want Scalekit to handle email deliverability **Default settings:** * **Sender Name**: Team workspace\_name * **From Email Address**: * **Infrastructure**: Fully managed by Scalekit No additional configuration is required. Your authentication emails will be sent automatically with these settings. Tip You can customize the sender name in your dashboard settings while still using Scalekit’s email infrastructure. ## Configure your own email provider [Section titled “Configure your own email provider”](#configure-your-own-email-provider) For production applications, you’ll likely want to use your own email provider to maintain brand consistency and control deliverability. When to use this approach: * You need emails sent from your domain * You want complete control over email deliverability * You need to meet compliance requirements (e.g. GDPR, CCPA) * You want to integrate with existing email analytics ### Gather your SMTP credentials [Section titled “Gather your SMTP credentials”](#gather-your-smtp-credentials) Before configuring, collect the following information 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 authentication password | | **Sender Email** | The email address emails will be sent from | | **Sender Name** | The display name recipients will see | ### Configure SMTP settings in Scalekit [Section titled “Configure SMTP settings in Scalekit”](#configure-smtp-settings-in-scalekit) 1. Navigate to email settings In your Scalekit dashboard, go to **Emails**. 2. Select custom email provider Choose **Use your own email provider** from the email delivery options 3. Configure sender information ```plaintext 1 From Email Address: noreply@yourdomain.com 2 Sender Name: Your Company Name ``` 4. Enter SMTP configuration ```plaintext 1 SMTP Server Host: smtp.your-provider.com 2 SMTP Port: 587 3 SMTP Username: your-username 4 SMTP Password: your-password ``` 5. Save and test configuration Click **Save** to apply your settings, then send a test email to verify the configuration ### Common provider configurations [Section titled “Common provider configurations”](#common-provider-configurations) * SendGrid ```plaintext 1 Host: smtp.sendgrid.net 2 Port: 587 3 Username: apikey 4 Password: [Your SendGrid API Key] ``` * Amazon SES ```plaintext 1 Host: email-smtp.us-east-1.amazonaws.com 2 Port: 587 3 Username: [Your SMTP Username from AWS] 4 Password: [Your SMTP Password from AWS] ``` * Postmark ```plaintext 1 Host: smtp.postmarkapp.com 2 Port: 587 3 Username: [Your Postmark Server Token] 4 Password: [Your Postmark Server Token] ``` Note All SMTP credentials are encrypted and stored securely. Email transmission uses TLS encryption for security. ## Test your email configuration [Section titled “Test your email configuration”](#test-your-email-configuration) After configuring your email provider, verify that everything works correctly: 1. Send a test email through your authentication flow 2. Check delivery to ensure emails reach the intended recipients 3. Verify sender information appears correctly in the recipient’s inbox 4. Confirm formatting, branding, links and buttons work as expected --- # DOCUMENT BOUNDARY --- # Authentication best practices > Security best practices for authentication implementation, including threat modeling, advanced patterns, and security checklists. This guide covers security best practices for implementing authentication with Scalekit. Use it for threat modeling, advanced security patterns, and production-ready configurations. ## Security threat model [Section titled “Security threat model”](#security-threat-model) ### Common authentication threats [Section titled “Common authentication threats”](#common-authentication-threats) Identify potential security threats to implement appropriate countermeasures: | Threat | Description | Mitigation | | -------------------- | ------------------------------------------------------------ | ------------------------------------------------- | | **CSRF attacks** | Malicious requests executed on behalf of authenticated users | Use `state` parameter, validate origins | | **Token theft** | Access tokens intercepted or stolen | Secure storage, short lifetimes, refresh rotation | | **Session fixation** | Attacker fixes session ID before authentication | Regenerate sessions, secure cookies | | **Phishing** | Users tricked into entering credentials on fake sites | Domain validation, HTTPS enforcement | | **Replay attacks** | Intercepted requests replayed by attackers | Nonces, timestamps, request signing | ### Multi-tenant security considerations [Section titled “Multi-tenant security considerations”](#multi-tenant-security-considerations) B2B applications face additional security challenges: * **Tenant isolation** - Prevent data leakage between organizations * **Admin privilege escalation** - Secure organization admin roles * **SSO configuration tampering** - Protect identity provider settings * **Cross-tenant user enumeration** - Prevent user discovery across organizations ## Advanced security patterns [Section titled “Advanced security patterns”](#advanced-security-patterns) ### Dynamic security policy enforcement [Section titled “Dynamic security policy enforcement”](#dynamic-security-policy-enforcement) Apply organization-specific security policies: * Node.js Dynamic security policies ```javascript 1 // Apply organization-specific security requirements 2 async function createAuthorizationUrl(orgId, userEmail) { 3 const redirectUri = 'https://yourapp.com/auth/callback'; 4 5 // Fetch organization security policy 6 const securityPolicy = await getSecurityPolicy(orgId); 7 8 // Apply conditional authentication requirements 9 const options = { 10 scopes: ['openid', 'profile', 'email', 'offline_access'], 11 organizationId: orgId, 12 loginHint: userEmail, 13 state: generateSecureState(), 14 15 // Force re-authentication for high-security orgs 16 prompt: securityPolicy.requireReauth ? 'login' : undefined, 17 maxAge: securityPolicy.maxSessionAge || 3600, 18 acrValues: securityPolicy.requiredAuthLevel || 'aal1' 19 }; 20 21 return scalekit.getAuthorizationUrl(redirectUri, options); 22 } ``` * Python Dynamic security policies ```python 1 # Apply organization-specific security requirements 2 async def create_authorization_url(org_id, user_email): 3 redirect_uri = 'https://yourapp.com/auth/callback' 4 5 # Fetch organization security policy 6 security_policy = await get_security_policy(org_id) 7 8 # Apply conditional authentication requirements 9 options = AuthorizationUrlOptions( 10 scopes=['openid', 'profile', 'email', 'offline_access'], 11 organization_id=org_id, 12 login_hint=user_email, 13 state=generate_secure_state(), 14 15 # Force re-authentication for high-security orgs 16 prompt='login' if security_policy.require_reauth else None, 17 max_age=security_policy.max_session_age or 3600, 18 acr_values=security_policy.required_auth_level or 'aal1' 19 ) 20 21 return scalekit.get_authorization_url(redirect_uri, options) ``` * Go Dynamic security policies ```go 1 // Apply organization-specific security requirements 2 func createAuthorizationUrl(orgId, userEmail string) (string, error) { 3 redirectUri := "https://yourapp.com/auth/callback" 4 5 // Fetch organization security policy 6 securityPolicy, err := getSecurityPolicy(orgId) 7 if err != nil { 8 return "", err 9 } 10 11 // Apply conditional authentication requirements 12 options := scalekit.AuthorizationUrlOptions{ 13 Scopes: []string{"openid", "profile", "email", "offline_access"}, 14 OrganizationId: orgId, 15 LoginHint: userEmail, 16 State: generateSecureState(), 17 18 // Force re-authentication for high-security orgs 19 Prompt: conditionalPrompt(securityPolicy.RequireReauth), 20 MaxAge: securityPolicy.MaxSessionAge, 21 AcrValues: securityPolicy.RequiredAuthLevel, 22 } 23 24 authUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options) 25 return authUrl.String(), err 26 } ``` * Java Dynamic security policies ```java 1 // Apply organization-specific security requirements 2 public String createAuthorizationUrl(String orgId, String userEmail) { 3 String redirectUri = "https://yourapp.com/auth/callback"; 4 5 // Fetch organization security policy 6 SecurityPolicy securityPolicy = getSecurityPolicy(orgId); 7 8 // Apply conditional authentication requirements 9 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 10 options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access")); 11 options.setOrganizationId(orgId); 12 options.setLoginHint(userEmail); 13 options.setState(generateSecureState()); 14 15 // Force re-authentication for high-security orgs 16 if (securityPolicy.isRequireReauth()) { 17 options.setPrompt("login"); 18 } 19 options.setMaxAge(securityPolicy.getMaxSessionAge()); 20 options.setAcrValues(securityPolicy.getRequiredAuthLevel()); 21 22 URL authUrl = scalekit.authentication().getAuthorizationUrl(redirectUri, options); 23 return authUrl.toString(); 24 } ``` ### Request signing and validation [Section titled “Request signing and validation”](#request-signing-and-validation) Verify request integrity with signatures: * Node.js Request signing ```javascript 1 const crypto = require('crypto'); 2 3 // Sign sensitive requests with HMAC 4 function signRequest(payload, secret) { 5 const timestamp = Date.now().toString(); 6 const nonce = crypto.randomBytes(16).toString('hex'); 7 8 // Create signature payload 9 const signaturePayload = `${timestamp}.${nonce}.${JSON.stringify(payload)}`; 10 const signature = crypto 11 .createHmac('sha256', secret) 12 .update(signaturePayload) 13 .digest('hex'); 14 15 return { 16 payload, 17 timestamp, 18 nonce, 19 signature: `sha256=${signature}` 20 }; 21 } 22 23 // Verify request signatures 24 function verifyRequest(receivedPayload, receivedSignature, secret, maxAge = 300) { 25 const [timestamp, nonce, payload] = receivedPayload.split('.'); 26 27 // Check timestamp to prevent replay attacks 28 if (Date.now() - parseInt(timestamp) > maxAge * 1000) { 29 throw new Error('Request timestamp too old'); 30 } 31 32 // Verify signature 33 const expectedPayload = `${timestamp}.${nonce}.${payload}`; 34 const expectedSignature = crypto 35 .createHmac('sha256', secret) 36 .update(expectedPayload) 37 .digest('hex'); 38 39 if (!crypto.timingSafeEqual( 40 Buffer.from(receivedSignature, 'hex'), 41 Buffer.from(`sha256=${expectedSignature}`, 'hex') 42 )) { 43 throw new Error('Invalid signature'); 44 } 45 46 return JSON.parse(payload); 47 } ``` * Python Request signing ```python 1 import hmac 2 import hashlib 3 import json 4 import time 5 import secrets 6 7 # Sign sensitive requests with HMAC 8 def sign_request(payload, secret): 9 timestamp = str(int(time.time() * 1000)) 10 nonce = secrets.token_hex(16) 11 12 # Create signature payload 13 signature_payload = f"{timestamp}.{nonce}.{json.dumps(payload)}" 14 signature = hmac.new( 15 secret.encode(), 16 signature_payload.encode(), 17 hashlib.sha256 18 ).hexdigest() 19 20 return { 21 'payload': payload, 22 'timestamp': timestamp, 23 'nonce': nonce, 24 'signature': f"sha256={signature}" 25 } 26 27 # Verify request signatures 28 def verify_request(received_payload, received_signature, secret, max_age=300): 29 timestamp, nonce, payload = received_payload.split('.') 30 31 # Check timestamp to prevent replay attacks 32 if time.time() * 1000 - int(timestamp) > max_age * 1000: 33 raise ValueError('Request timestamp too old') 34 35 # Verify signature 36 expected_payload = f"{timestamp}.{nonce}.{payload}" 37 expected_signature = hmac.new( 38 secret.encode(), 39 expected_payload.encode(), 40 hashlib.sha256 41 ).hexdigest() 42 43 if not hmac.compare_digest( 44 received_signature, 45 f"sha256={expected_signature}" 46 ): 47 raise ValueError('Invalid signature') 48 49 return json.loads(payload) ``` * Go Request signing ```go 1 import ( 2 "crypto/hmac" 3 "crypto/rand" 4 "crypto/sha256" 5 "encoding/hex" 6 "encoding/json" 7 "fmt" 8 "time" 9 ) 10 11 // Sign sensitive requests with HMAC 12 func signRequest(payload interface{}, secret string) (map[string]interface{}, error) { 13 timestamp := fmt.Sprintf("%d", time.Now().UnixMilli()) 14 15 nonceBytes := make([]byte, 16) 16 rand.Read(nonceBytes) 17 nonce := hex.EncodeToString(nonceBytes) 18 19 // Create signature payload 20 payloadJSON, _ := json.Marshal(payload) 21 signaturePayload := fmt.Sprintf("%s.%s.%s", timestamp, nonce, payloadJSON) 22 23 h := hmac.New(sha256.New, []byte(secret)) 24 h.Write([]byte(signaturePayload)) 25 signature := hex.EncodeToString(h.Sum(nil)) 26 27 return map[string]interface{}{ 28 "payload": payload, 29 "timestamp": timestamp, 30 "nonce": nonce, 31 "signature": fmt.Sprintf("sha256=%s", signature), 32 }, nil 33 } 34 35 // Verify request signatures 36 func verifyRequest(receivedPayload, receivedSignature, secret string, maxAge int64) (interface{}, error) { 37 // Parse payload components 38 parts := strings.Split(receivedPayload, ".") 39 if len(parts) != 3 { 40 return nil, fmt.Errorf("invalid payload format") 41 } 42 43 timestamp, err := strconv.ParseInt(parts[0], 10, 64) 44 if err != nil { 45 return nil, fmt.Errorf("invalid timestamp") 46 } 47 48 // Check timestamp to prevent replay attacks 49 if time.Now().UnixMilli()-timestamp > maxAge*1000 { 50 return nil, fmt.Errorf("request timestamp too old") 51 } 52 53 // Verify signature 54 expectedPayload := receivedPayload 55 h := hmac.New(sha256.New, []byte(secret)) 56 h.Write([]byte(expectedPayload)) 57 expectedSignature := fmt.Sprintf("sha256=%s", hex.EncodeToString(h.Sum(nil))) 58 59 if !hmac.Equal([]byte(receivedSignature), []byte(expectedSignature)) { 60 return nil, fmt.Errorf("invalid signature") 61 } 62 63 var payload interface{} 64 if err := json.Unmarshal([]byte(parts[2]), &payload); err != nil { 65 return nil, fmt.Errorf("invalid payload JSON") 66 } 67 68 return payload, nil 69 } ``` * Java Request signing ```java 1 import javax.crypto.Mac; 2 import javax.crypto.spec.SecretKeySpec; 3 import java.security.SecureRandom; 4 import java.nio.charset.StandardCharsets; 5 import java.util.HashMap; 6 import java.util.Map; 7 8 // Sign sensitive requests with HMAC 9 public Map signRequest(Object payload, String secret) throws Exception { 10 String timestamp = String.valueOf(System.currentTimeMillis()); 11 12 SecureRandom random = new SecureRandom(); 13 byte[] nonceBytes = new byte[16]; 14 random.nextBytes(nonceBytes); 15 String nonce = bytesToHex(nonceBytes); 16 17 // Create signature payload 18 String payloadJson = objectMapper.writeValueAsString(payload); 19 String signaturePayload = timestamp + "." + nonce + "." + payloadJson; 20 21 Mac mac = Mac.getInstance("HmacSHA256"); 22 SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); 23 mac.init(secretKey); 24 byte[] signatureBytes = mac.doFinal(signaturePayload.getBytes(StandardCharsets.UTF_8)); 25 String signature = "sha256=" + bytesToHex(signatureBytes); 26 27 Map result = new HashMap<>(); 28 result.put("payload", payload); 29 result.put("timestamp", timestamp); 30 result.put("nonce", nonce); 31 result.put("signature", signature); 32 33 return result; 34 } 35 36 // Verify request signatures 37 public Object verifyRequest(String receivedPayload, String receivedSignature, 38 String secret, long maxAge) throws Exception { 39 String[] parts = receivedPayload.split("\\."); 40 if (parts.length != 3) { 41 throw new SecurityException("Invalid payload format"); 42 } 43 44 long timestamp = Long.parseLong(parts[0]); 45 46 // Check timestamp to prevent replay attacks 47 if (System.currentTimeMillis() - timestamp > maxAge * 1000) { 48 throw new SecurityException("Request timestamp too old"); 49 } 50 51 // Verify signature 52 Mac mac = Mac.getInstance("HmacSHA256"); 53 SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); 54 mac.init(secretKey); 55 byte[] expectedSignatureBytes = mac.doFinal(receivedPayload.getBytes(StandardCharsets.UTF_8)); 56 String expectedSignature = "sha256=" + bytesToHex(expectedSignatureBytes); 57 58 if (!MessageDigest.isEqual( 59 receivedSignature.getBytes(StandardCharsets.UTF_8), 60 expectedSignature.getBytes(StandardCharsets.UTF_8) 61 )) { 62 throw new SecurityException("Invalid signature"); 63 } 64 65 return objectMapper.readValue(parts[2], Object.class); 66 } ``` ## Secure token management [Section titled “Secure token management”](#secure-token-management) ### Token storage strategies [Section titled “Token storage strategies”](#token-storage-strategies) Select storage methods based on your application architecture: | Storage Method | Security Level | Use Case | Considerations | | --------------------- | -------------- | ------------------- | -------------------------------------- | | **HTTP-only cookies** | High | Web applications | Prevents XSS, requires CSRF protection | | **Secure memory** | High | Mobile/desktop apps | Cleared on app termination | | **Encrypted storage** | Medium | Persistent sessions | Key management complexity | | **LocalStorage** | Low | Not recommended | Vulnerable to XSS attacks | ### Token rotation implementation [Section titled “Token rotation implementation”](#token-rotation-implementation) Implement secure refresh token rotation: * Node.js Token rotation ```javascript 1 // Secure token refresh with rotation 2 async function refreshAccessToken(refreshToken, userId) { 3 try { 4 // Exchange refresh token for new tokens 5 const tokenResponse = await scalekit.exchangeCodeForTokens({ 6 refresh_token: refreshToken, 7 grant_type: 'refresh_token' 8 }); 9 10 // Store new tokens securely 11 const newTokens = { 12 accessToken: tokenResponse.access_token, 13 refreshToken: tokenResponse.refresh_token, // New refresh token 14 expiresAt: Date.now() + (tokenResponse.expires_in * 1000), 15 refreshExpiresAt: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days 16 }; 17 18 // Update token storage atomically 19 await updateUserTokens(userId, newTokens); 20 21 // Invalidate old refresh token 22 await invalidateRefreshToken(refreshToken); 23 24 return newTokens; 25 26 } catch (error) { 27 // Handle refresh failure 28 if (error.code === 'invalid_grant') { 29 // Refresh token expired or revoked 30 await logoutUser(userId); 31 throw new Error('Session expired, please login again'); 32 } 33 34 // Log security event 35 await logSecurityEvent('token_refresh_failed', { 36 userId, 37 error: error.message, 38 timestamp: new Date().toISOString() 39 }); 40 41 throw error; 42 } 43 } 44 45 // Automatic token refresh middleware 46 function autoRefreshMiddleware(req, res, next) { 47 const { accessToken, refreshToken, expiresAt } = req.session.tokens || {}; 48 49 // Check if token expires within 5 minutes 50 if (accessToken && Date.now() + (5 * 60 * 1000) >= expiresAt) { 51 refreshAccessToken(refreshToken, req.session.userId) 52 .then(newTokens => { 53 req.session.tokens = newTokens; 54 next(); 55 }) 56 .catch(error => { 57 // Clear session on refresh failure 58 req.session.destroy(); 59 res.status(401).json({ error: 'Authentication required' }); 60 }); 61 } else { 62 next(); 63 } 64 } ``` * Python Token rotation ```python 1 import asyncio 2 from datetime import datetime, timedelta 3 4 # Secure token refresh with rotation 5 async def refresh_access_token(refresh_token, user_id): 6 try: 7 # Exchange refresh token for new tokens 8 token_response = await scalekit.exchange_code_for_tokens({ 9 'refresh_token': refresh_token, 10 'grant_type': 'refresh_token' 11 }) 12 13 # Store new tokens securely 14 new_tokens = { 15 'access_token': token_response['access_token'], 16 'refresh_token': token_response['refresh_token'], # New refresh token 17 'expires_at': datetime.now() + timedelta(seconds=token_response['expires_in']), 18 'refresh_expires_at': datetime.now() + timedelta(days=30) 19 } 20 21 # Update token storage atomically 22 await update_user_tokens(user_id, new_tokens) 23 24 # Invalidate old refresh token 25 await invalidate_refresh_token(refresh_token) 26 27 return new_tokens 28 29 except Exception as error: 30 # Handle refresh failure 31 if hasattr(error, 'code') and error.code == 'invalid_grant': 32 # Refresh token expired or revoked 33 await logout_user(user_id) 34 raise Exception('Session expired, please login again') 35 36 # Log security event 37 await log_security_event('token_refresh_failed', { 38 'user_id': user_id, 39 'error': str(error), 40 'timestamp': datetime.now().isoformat() 41 }) 42 43 raise error 44 45 # Automatic token refresh decorator 46 def auto_refresh_tokens(func): 47 async def wrapper(*args, **kwargs): 48 request = kwargs.get('request') or args[0] 49 tokens = getattr(request.session, 'tokens', {}) 50 51 access_token = tokens.get('access_token') 52 refresh_token = tokens.get('refresh_token') 53 expires_at = tokens.get('expires_at') 54 55 # Check if token expires within 5 minutes 56 if access_token and expires_at and datetime.now() + timedelta(minutes=5) >= expires_at: 57 try: 58 new_tokens = await refresh_access_token(refresh_token, request.session.user_id) 59 request.session.tokens = new_tokens 60 except Exception: 61 # Clear session on refresh failure 62 request.session.clear() 63 raise AuthenticationError('Authentication required') 64 65 return await func(*args, **kwargs) 66 return wrapper ``` * Go Token rotation ```go 1 import ( 2 "context" 3 "fmt" 4 "time" 5 ) 6 7 type TokenSet struct { 8 AccessToken string `json:"access_token"` 9 RefreshToken string `json:"refresh_token"` 10 ExpiresAt time.Time `json:"expires_at"` 11 RefreshExpiresAt time.Time `json:"refresh_expires_at"` 12 } 13 14 // Secure token refresh with rotation 15 func refreshAccessToken(ctx context.Context, refreshToken, userID string) (*TokenSet, error) { 16 // Exchange refresh token for new tokens 17 tokenResponse, err := scalekit.ExchangeCodeForTokens(ctx, &scalekit.TokenRequest{ 18 RefreshToken: refreshToken, 19 GrantType: "refresh_token", 20 }) 21 if err != nil { 22 return nil, fmt.Errorf("token exchange failed: %w", err) 23 } 24 25 // Store new tokens securely 26 newTokens := &TokenSet{ 27 AccessToken: tokenResponse.AccessToken, 28 RefreshToken: tokenResponse.RefreshToken, // New refresh token 29 ExpiresAt: time.Now().Add(time.Duration(tokenResponse.ExpiresIn) * time.Second), 30 RefreshExpiresAt: time.Now().Add(30 * 24 * time.Hour), // 30 days 31 } 32 33 // Update token storage atomically 34 if err := updateUserTokens(ctx, userID, newTokens); err != nil { 35 return nil, fmt.Errorf("failed to update tokens: %w", err) 36 } 37 38 // Invalidate old refresh token 39 if err := invalidateRefreshToken(ctx, refreshToken); err != nil { 40 // Log but don't fail the operation 41 logSecurityEvent(ctx, "refresh_token_invalidation_failed", map[string]interface{}{ 42 "user_id": userID, 43 "error": err.Error(), 44 }) 45 } 46 47 return newTokens, nil 48 } 49 50 // Automatic token refresh middleware 51 func autoRefreshMiddleware(next http.Handler) http.Handler { 52 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 53 session := getSession(r) 54 tokens := session.Tokens 55 56 // Check if token expires within 5 minutes 57 if tokens != nil && time.Until(tokens.ExpiresAt) <= 5*time.Minute { 58 newTokens, err := refreshAccessToken(r.Context(), tokens.RefreshToken, session.UserID) 59 if err != nil { 60 // Clear session on refresh failure 61 clearSession(w, r) 62 http.Error(w, "Authentication required", http.StatusUnauthorized) 63 return 64 } 65 66 session.Tokens = newTokens 67 saveSession(w, r, session) 68 } 69 70 next.ServeHTTP(w, r) 71 }) 72 } ``` * Java Token rotation ```java 1 import java.time.Instant; 2 import java.time.temporal.ChronoUnit; 3 import java.util.concurrent.CompletableFuture; 4 5 public class TokenSet { 6 private String accessToken; 7 private String refreshToken; 8 private Instant expiresAt; 9 private Instant refreshExpiresAt; 10 11 // constructors, getters, setters... 12 } 13 14 // Secure token refresh with rotation 15 public CompletableFuture refreshAccessToken(String refreshToken, String userId) { 16 return CompletableFuture.supplyAsync(() -> { 17 try { 18 // Exchange refresh token for new tokens 19 TokenResponse tokenResponse = scalekit.authentication() 20 .exchangeCodeForTokens(TokenRequest.builder() 21 .refreshToken(refreshToken) 22 .grantType("refresh_token") 23 .build()); 24 25 // Store new tokens securely 26 TokenSet newTokens = new TokenSet(); 27 newTokens.setAccessToken(tokenResponse.getAccessToken()); 28 newTokens.setRefreshToken(tokenResponse.getRefreshToken()); // New refresh token 29 newTokens.setExpiresAt(Instant.now().plusSeconds(tokenResponse.getExpiresIn())); 30 newTokens.setRefreshExpiresAt(Instant.now().plus(30, ChronoUnit.DAYS)); 31 32 // Update token storage atomically 33 updateUserTokens(userId, newTokens); 34 35 // Invalidate old refresh token 36 invalidateRefreshToken(refreshToken); 37 38 return newTokens; 39 40 } catch (Exception e) { 41 // Handle refresh failure 42 if (e instanceof InvalidGrantException) { 43 // Refresh token expired or revoked 44 logoutUser(userId); 45 throw new AuthenticationException("Session expired, please login again"); 46 } 47 48 // Log security event 49 logSecurityEvent("token_refresh_failed", Map.of( 50 "user_id", userId, 51 "error", e.getMessage(), 52 "timestamp", Instant.now().toString() 53 )); 54 55 throw new RuntimeException(e); 56 } 57 }); 58 } 59 60 // Automatic token refresh interceptor 61 @Component 62 public class AutoRefreshInterceptor implements HandlerInterceptor { 63 64 @Override 65 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 66 Object handler) throws Exception { 67 HttpSession session = request.getSession(false); 68 if (session == null) return true; 69 70 TokenSet tokens = (TokenSet) session.getAttribute("tokens"); 71 if (tokens == null) return true; 72 73 // Check if token expires within 5 minutes 74 if (tokens.getExpiresAt().minus(5, ChronoUnit.MINUTES).isBefore(Instant.now())) { 75 try { 76 String userId = (String) session.getAttribute("userId"); 77 TokenSet newTokens = refreshAccessToken(tokens.getRefreshToken(), userId).get(); 78 session.setAttribute("tokens", newTokens); 79 } catch (Exception e) { 80 // Clear session on refresh failure 81 session.invalidate(); 82 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 83 response.getWriter().write("{\"error\":\"Authentication required\"}"); 84 return false; 85 } 86 } 87 88 return true; 89 } 90 } ``` ## Security monitoring and incident response [Section titled “Security monitoring and incident response”](#security-monitoring-and-incident-response) ### Security event logging [Section titled “Security event logging”](#security-event-logging) Log security events for monitoring and analysis: security-events.js ```javascript 1 // Define security event types 2 const SECURITY_EVENTS = { 3 LOGIN_SUCCESS: 'login_success', 4 LOGIN_FAILURE: 'login_failure', 5 TOKEN_REFRESH: 'token_refresh', 6 SUSPICIOUS_ACTIVITY: 'suspicious_activity', 7 PRIVILEGE_ESCALATION: 'privilege_escalation', 8 DATA_ACCESS: 'sensitive_data_access' 9 }; 10 11 // Security event logger 12 async function logSecurityEvent(eventType, details) { 13 const event = { 14 type: eventType, 15 timestamp: new Date().toISOString(), 16 severity: getSeverityLevel(eventType), 17 details: { 18 ...details, 19 userAgent: details.userAgent, 20 ipAddress: details.ipAddress, 21 sessionId: details.sessionId 22 } 23 }; 24 25 // Store in security log 26 await securityLogger.log(event); 27 28 // Trigger alerts for high-severity events 29 if (event.severity === 'HIGH' || event.severity === 'CRITICAL') { 30 await triggerSecurityAlert(event); 31 } 32 } 33 34 // Anomaly detection 35 async function detectAnomalies(userId, loginEvent) { 36 const recentLogins = await getRecentLogins(userId, '24h'); 37 38 // Check for unusual patterns 39 const anomalies = []; 40 41 // Geographic anomaly 42 if (isUnusualLocation(loginEvent.location, recentLogins)) { 43 anomalies.push('unusual_location'); 44 } 45 46 // Time-based anomaly 47 if (isUnusualTime(loginEvent.timestamp, recentLogins)) { 48 anomalies.push('unusual_time'); 49 } 50 51 // Device anomaly 52 if (isUnusualDevice(loginEvent.device, recentLogins)) { 53 anomalies.push('unusual_device'); 54 } 55 56 if (anomalies.length > 0) { 57 await logSecurityEvent(SECURITY_EVENTS.SUSPICIOUS_ACTIVITY, { 58 userId, 59 anomalies, 60 loginEvent 61 }); 62 } 63 64 return anomalies; 65 } ``` ### Rate limiting and abuse prevention [Section titled “Rate limiting and abuse prevention”](#rate-limiting-and-abuse-prevention) Apply rate limiting to prevent abuse: * Node.js Advanced rate limiting ```javascript 1 // Multi-tier rate limiting 2 class SecurityRateLimiter { 3 constructor() { 4 this.limits = { 5 // Per-IP limits 6 login_attempts: { window: 900, max: 10 }, // 10 attempts per 15 min 7 token_requests: { window: 3600, max: 100 }, // 100 requests per hour 8 9 // Per-user limits 10 user_login_attempts: { window: 3600, max: 5 }, // 5 attempts per hour 11 user_token_refresh: { window: 3600, max: 50 }, // 50 refreshes per hour 12 13 // Global limits 14 total_requests: { window: 60, max: 10000 } // 10k requests per minute 15 }; 16 } 17 18 async checkLimit(type, identifier, customLimit = null) { 19 const limit = customLimit || this.limits[type]; 20 if (!limit) return { allowed: true }; 21 22 const key = `${type}:${identifier}`; 23 const current = await redis.get(key) || 0; 24 25 if (current >= limit.max) { 26 await this.logRateLimitExceeded(type, identifier, current); 27 return { 28 allowed: false, 29 retryAfter: await redis.ttl(key), 30 current: current, 31 max: limit.max 32 }; 33 } 34 35 // Increment counter with expiration 36 await redis.multi() 37 .incr(key) 38 .expire(key, limit.window) 39 .exec(); 40 41 return { allowed: true, current: current + 1, max: limit.max }; 42 } 43 44 // Dynamic rate limiting based on risk 45 async getDynamicLimit(type, riskScore) { 46 const baseLimit = this.limits[type]; 47 if (riskScore > 0.8) { 48 return { ...baseLimit, max: Math.floor(baseLimit.max * 0.2) }; 49 } else if (riskScore > 0.6) { 50 return { ...baseLimit, max: Math.floor(baseLimit.max * 0.5) }; 51 } 52 return baseLimit; 53 } 54 } 55 56 // Rate limiting middleware 57 async function rateLimitMiddleware(req, res, next) { 58 const limiter = new SecurityRateLimiter(); 59 const clientIP = req.ip; 60 const userId = req.session?.userId; 61 62 // Check IP-based limits 63 const ipLimit = await limiter.checkLimit('login_attempts', clientIP); 64 if (!ipLimit.allowed) { 65 return res.status(429).json({ 66 error: 'Too many requests', 67 retryAfter: ipLimit.retryAfter 68 }); 69 } 70 71 // Check user-based limits if authenticated 72 if (userId) { 73 const userLimit = await limiter.checkLimit('user_login_attempts', userId); 74 if (!userLimit.allowed) { 75 return res.status(429).json({ 76 error: 'Too many login attempts', 77 retryAfter: userLimit.retryAfter 78 }); 79 } 80 } 81 82 next(); 83 } ``` * Python Advanced rate limiting ```python 1 import asyncio 2 import time 3 from typing import Dict, Optional 4 5 class SecurityRateLimiter: 6 def __init__(self): 7 self.limits = { 8 # Per-IP limits 9 'login_attempts': {'window': 900, 'max': 10}, # 10 attempts per 15 min 10 'token_requests': {'window': 3600, 'max': 100}, # 100 requests per hour 11 12 # Per-user limits 13 'user_login_attempts': {'window': 3600, 'max': 5}, # 5 attempts per hour 14 'user_token_refresh': {'window': 3600, 'max': 50}, # 50 refreshes per hour 15 16 # Global limits 17 'total_requests': {'window': 60, 'max': 10000} # 10k requests per minute 18 } 19 20 async def check_limit(self, limit_type: str, identifier: str, custom_limit: Optional[Dict] = None): 21 limit = custom_limit or self.limits.get(limit_type) 22 if not limit: 23 return {'allowed': True} 24 25 key = f"{limit_type}:{identifier}" 26 current = await redis.get(key) or 0 27 current = int(current) 28 29 if current >= limit['max']: 30 await self.log_rate_limit_exceeded(limit_type, identifier, current) 31 ttl = await redis.ttl(key) 32 return { 33 'allowed': False, 34 'retry_after': ttl, 35 'current': current, 36 'max': limit['max'] 37 } 38 39 # Increment counter with expiration 40 pipeline = redis.pipeline() 41 pipeline.incr(key) 42 pipeline.expire(key, limit['window']) 43 await pipeline.execute() 44 45 return {'allowed': True, 'current': current + 1, 'max': limit['max']} 46 47 # Dynamic rate limiting based on risk 48 async def get_dynamic_limit(self, limit_type: str, risk_score: float): 49 base_limit = self.limits[limit_type].copy() 50 if risk_score > 0.8: 51 base_limit['max'] = int(base_limit['max'] * 0.2) 52 elif risk_score > 0.6: 53 base_limit['max'] = int(base_limit['max'] * 0.5) 54 return base_limit 55 56 # Rate limiting decorator 57 def rate_limit(limit_type: str): 58 def decorator(func): 59 async def wrapper(*args, **kwargs): 60 request = kwargs.get('request') or args[0] 61 limiter = SecurityRateLimiter() 62 client_ip = request.client.host 63 user_id = getattr(request.session, 'user_id', None) 64 65 # Check IP-based limits 66 ip_limit = await limiter.check_limit(limit_type, client_ip) 67 if not ip_limit['allowed']: 68 raise HTTPException( 69 status_code=429, 70 detail={ 71 'error': 'Too many requests', 72 'retry_after': ip_limit['retry_after'] 73 } 74 ) 75 76 # Check user-based limits if authenticated 77 if user_id: 78 user_limit = await limiter.check_limit(f'user_{limit_type}', user_id) 79 if not user_limit['allowed']: 80 raise HTTPException( 81 status_code=429, 82 detail={ 83 'error': 'Too many attempts', 84 'retry_after': user_limit['retry_after'] 85 } 86 ) 87 88 return await func(*args, **kwargs) 89 return wrapper 90 return decorator ``` * Go Advanced rate limiting ```go 1 import ( 2 "context" 3 "fmt" 4 "time" 5 ) 6 7 type RateLimit struct { 8 Window time.Duration 9 Max int 10 } 11 12 type SecurityRateLimiter struct { 13 limits map[string]RateLimit 14 redis RedisClient 15 } 16 17 func NewSecurityRateLimiter(redis RedisClient) *SecurityRateLimiter { 18 return &SecurityRateLimiter{ 19 redis: redis, 20 limits: map[string]RateLimit{ 21 // Per-IP limits 22 "login_attempts": {Window: 15 * time.Minute, Max: 10}, 23 "token_requests": {Window: time.Hour, Max: 100}, 24 25 // Per-user limits 26 "user_login_attempts": {Window: time.Hour, Max: 5}, 27 "user_token_refresh": {Window: time.Hour, Max: 50}, 28 29 // Global limits 30 "total_requests": {Window: time.Minute, Max: 10000}, 31 }, 32 } 33 } 34 35 type LimitResult struct { 36 Allowed bool 37 RetryAfter int64 38 Current int 39 Max int 40 } 41 42 func (rl *SecurityRateLimiter) CheckLimit(ctx context.Context, limitType, identifier string, customLimit *RateLimit) (*LimitResult, error) { 43 limit := customLimit 44 if limit == nil { 45 l, exists := rl.limits[limitType] 46 if !exists { 47 return &LimitResult{Allowed: true}, nil 48 } 49 limit = &l 50 } 51 52 key := fmt.Sprintf("%s:%s", limitType, identifier) 53 current, err := rl.redis.Get(ctx, key).Int() 54 if err != nil && err != redis.Nil { 55 return nil, err 56 } 57 58 if current >= limit.Max { 59 ttl, _ := rl.redis.TTL(ctx, key).Result() 60 await rl.logRateLimitExceeded(limitType, identifier, current) 61 return &LimitResult{ 62 Allowed: false, 63 RetryAfter: int64(ttl.Seconds()), 64 Current: current, 65 Max: limit.Max, 66 }, nil 67 } 68 69 // Increment counter with expiration 70 pipe := rl.redis.Pipeline() 71 pipe.Incr(ctx, key) 72 pipe.Expire(ctx, key, limit.Window) 73 _, err = pipe.Exec(ctx) 74 if err != nil { 75 return nil, err 76 } 77 78 return &LimitResult{ 79 Allowed: true, 80 Current: current + 1, 81 Max: limit.Max, 82 }, nil 83 } 84 85 // Dynamic rate limiting based on risk 86 func (rl *SecurityRateLimiter) GetDynamicLimit(limitType string, riskScore float64) *RateLimit { 87 baseLimit, exists := rl.limits[limitType] 88 if !exists { 89 return nil 90 } 91 92 if riskScore > 0.8 { 93 return &RateLimit{ 94 Window: baseLimit.Window, 95 Max: int(float64(baseLimit.Max) * 0.2), 96 } 97 } else if riskScore > 0.6 { 98 return &RateLimit{ 99 Window: baseLimit.Window, 100 Max: int(float64(baseLimit.Max) * 0.5), 101 } 102 } 103 104 return &baseLimit 105 } 106 107 // Rate limiting middleware 108 func (rl *SecurityRateLimiter) RateLimitMiddleware(limitType string) gin.HandlerFunc { 109 return func(c *gin.Context) { 110 clientIP := c.ClientIP() 111 userID, _ := c.Get("userID") 112 113 // Check IP-based limits 114 ipLimit, err := rl.CheckLimit(c.Request.Context(), limitType, clientIP, nil) 115 if err != nil { 116 c.JSON(500, gin.H{"error": "Internal server error"}) 117 c.Abort() 118 return 119 } 120 121 if !ipLimit.Allowed { 122 c.JSON(429, gin.H{ 123 "error": "Too many requests", 124 "retry_after": ipLimit.RetryAfter, 125 }) 126 c.Abort() 127 return 128 } 129 130 // Check user-based limits if authenticated 131 if userID != nil { 132 userLimit, err := rl.CheckLimit(c.Request.Context(), "user_"+limitType, userID.(string), nil) 133 if err != nil { 134 c.JSON(500, gin.H{"error": "Internal server error"}) 135 c.Abort() 136 return 137 } 138 139 if !userLimit.Allowed { 140 c.JSON(429, gin.H{ 141 "error": "Too many attempts", 142 "retry_after": userLimit.RetryAfter, 143 }) 144 c.Abort() 145 return 146 } 147 } 148 149 c.Next() 150 } 151 } ``` * Java Advanced rate limiting ```java 1 import java.time.Duration; 2 import java.time.Instant; 3 import java.util.Map; 4 import java.util.HashMap; 5 import java.util.concurrent.CompletableFuture; 6 7 public class RateLimit { 8 private final Duration window; 9 private final int max; 10 11 // constructors, getters... 12 } 13 14 @Component 15 public class SecurityRateLimiter { 16 private final Map limits; 17 private final RedisTemplate redisTemplate; 18 19 public SecurityRateLimiter(RedisTemplate redisTemplate) { 20 this.redisTemplate = redisTemplate; 21 this.limits = Map.of( 22 // Per-IP limits 23 "login_attempts", new RateLimit(Duration.ofMinutes(15), 10), 24 "token_requests", new RateLimit(Duration.ofHours(1), 100), 25 26 // Per-user limits 27 "user_login_attempts", new RateLimit(Duration.ofHours(1), 5), 28 "user_token_refresh", new RateLimit(Duration.ofHours(1), 50), 29 30 // Global limits 31 "total_requests", new RateLimit(Duration.ofMinutes(1), 10000) 32 ); 33 } 34 35 public static class LimitResult { 36 private final boolean allowed; 37 private final long retryAfter; 38 private final int current; 39 private final int max; 40 41 // constructors, getters... 42 } 43 44 public CompletableFuture checkLimit(String limitType, String identifier, RateLimit customLimit) { 45 return CompletableFuture.supplyAsync(() -> { 46 RateLimit limit = customLimit != null ? customLimit : limits.get(limitType); 47 if (limit == null) { 48 return new LimitResult(true, 0, 0, 0); 49 } 50 51 String key = limitType + ":" + identifier; 52 String currentStr = redisTemplate.opsForValue().get(key); 53 int current = currentStr != null ? Integer.parseInt(currentStr) : 0; 54 55 if (current >= limit.getMax()) { 56 Long ttl = redisTemplate.getExpire(key); 57 logRateLimitExceeded(limitType, identifier, current); 58 return new LimitResult(false, ttl, current, limit.getMax()); 59 } 60 61 // Increment counter with expiration 62 redisTemplate.opsForValue().increment(key); 63 redisTemplate.expire(key, limit.getWindow()); 64 65 return new LimitResult(true, 0, current + 1, limit.getMax()); 66 }); 67 } 68 69 // Dynamic rate limiting based on risk 70 public RateLimit getDynamicLimit(String limitType, double riskScore) { 71 RateLimit baseLimit = limits.get(limitType); 72 if (baseLimit == null) return null; 73 74 if (riskScore > 0.8) { 75 return new RateLimit(baseLimit.getWindow(), (int) (baseLimit.getMax() * 0.2)); 76 } else if (riskScore > 0.6) { 77 return new RateLimit(baseLimit.getWindow(), (int) (baseLimit.getMax() * 0.5)); 78 } 79 80 return baseLimit; 81 } 82 } 83 84 // Rate limiting interceptor 85 @Component 86 public class RateLimitInterceptor implements HandlerInterceptor { 87 88 private final SecurityRateLimiter rateLimiter; 89 90 public RateLimitInterceptor(SecurityRateLimiter rateLimiter) { 91 this.rateLimiter = rateLimiter; 92 } 93 94 @Override 95 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 96 Object handler) throws Exception { 97 String clientIP = getClientIP(request); 98 String userID = getUserID(request); 99 100 // Check IP-based limits 101 LimitResult ipLimit = rateLimiter.checkLimit("login_attempts", clientIP, null).get(); 102 if (!ipLimit.isAllowed()) { 103 response.setStatus(429); 104 response.getWriter().write(String.format( 105 "{\"error\":\"Too many requests\",\"retry_after\":%d}", 106 ipLimit.getRetryAfter() 107 )); 108 return false; 109 } 110 111 // Check user-based limits if authenticated 112 if (userID != null) { 113 LimitResult userLimit = rateLimiter.checkLimit("user_login_attempts", userID, null).get(); 114 if (!userLimit.isAllowed()) { 115 response.setStatus(429); 116 response.getWriter().write(String.format( 117 "{\"error\":\"Too many attempts\",\"retry_after\":%d}", 118 userLimit.getRetryAfter() 119 )); 120 return false; 121 } 122 } 123 124 return true; 125 } 126 127 private String getClientIP(HttpServletRequest request) { 128 String xForwardedFor = request.getHeader("X-Forwarded-For"); 129 if (xForwardedFor != null && !xForwardedFor.isEmpty()) { 130 return xForwardedFor.split(",")[0].trim(); 131 } 132 return request.getRemoteAddr(); 133 } 134 135 private String getUserID(HttpServletRequest request) { 136 HttpSession session = request.getSession(false); 137 return session != null ? (String) session.getAttribute("userID") : null; 138 } 139 } ``` ## Production security checklist [Section titled “Production security checklist”](#production-security-checklist) ### Pre-deployment validation [Section titled “Pre-deployment validation”](#pre-deployment-validation) 1. **Environment security** * \[ ] All secrets stored in secure environment variables * \[ ] HTTPS enforced in production (no mixed content) * \[ ] Security headers configured (HSTS, CSP, X-Frame-Options) * \[ ] Database connections encrypted 2. **Authentication configuration** * \[ ] Redirect URIs validated and restricted * \[ ] Token lifetimes appropriate for security requirements * \[ ] Refresh token rotation enabled * \[ ] State parameter validation implemented 3. **Session management** * \[ ] Secure session storage configured * \[ ] Session timeout policies defined * \[ ] Concurrent session limits set * \[ ] Session invalidation on logout 4. **Rate limiting and monitoring** * \[ ] Rate limiting configured for all auth endpoints * \[ ] Security event logging implemented * \[ ] Anomaly detection systems deployed * \[ ] Alert systems configured ### Security testing procedures [Section titled “Security testing procedures”](#security-testing-procedures) Test security measures before production deployment: Security testing commands ```bash 1 # OWASP ZAP security scan 2 zap-cli quick-scan --self-contained \ 3 --start-options '-config api.disablekey=true' \ 4 https://your-app.com 5 6 # SSL/TLS configuration test 7 testssl --full https://your-app.com 8 9 # CSRF protection test 10 curl -X POST https://your-app.com/auth/login \ 11 -H "Content-Type: application/json" \ 12 -d '{"email":"test@example.com"}' 13 14 # Rate limiting test 15 for i in {1..20}; do 16 curl -X POST https://your-app.com/auth/login \ 17 -H "Content-Type: application/json" \ 18 -d '{"email":"test@example.com","password":"wrong"}' 19 done ``` ### Incident response procedures [Section titled “Incident response procedures”](#incident-response-procedures) Define procedures for handling security incidents: 1. **Detection** - Automated alerts for suspicious activities 2. **Assessment** - Rapid impact evaluation and threat classification 3. **Containment** - Immediate actions to limit damage 4. **Investigation** - Forensic analysis and root cause identification 5. **Recovery** - System restoration and security improvements 6. **Communication** - Stakeholder notifications and compliance reporting Security is an ongoing process Security implementation continues after deployment. Review and update security measures regularly, monitor for new threats, and maintain incident response capabilities. ### Production requirements [Section titled “Production requirements”](#production-requirements) * **Use HTTPS** - Required in production for secure token transmission * **Store tokens securely** - Use HTTP-only cookies or secure server-side storage * **Validate redirects** - Configure allowed redirect URIs in your dashboard This guide provides the foundation for implementing robust authentication security. Combine these patterns with regular security assessments and stay updated on emerging threats. --- # DOCUMENT BOUNDARY --- # Migrate SSO without IdP reconfiguration for customers > Learn how to coexist with external SSO providers while gradually migrating to Scalekit's SSO solution Single Sign-On capability of your application allows users in your customer’s organizations to access your application using their existing credentials. In this guide, you will migrate SSO connections to Scalekit without requiring customers to reconfigure their identity providers from their existing SSO provider solutions such as Auth0 or WorkOS. ### Prerequisites [Section titled “Prerequisites”](#prerequisites) 1. You control DNS for your auth domain, and its CNAME points to your external SSO provider. 2. Scalekit is set up — you have [signed up](https://app.scalekit.com) and installed the [Scalekit SDK](https://docs.scalekit.com/authenticate/fsa/quickstart/#install-the-scalekit-sdk). Verify custom domain configurations Some existing customers will have configured their identity provider with necessary settings such as **SP Entity ID** and **ACS URL**. These should start with a domain that you own such as `auth.yourapp.com/rest/of/the/path` where CNAME is correctly configured with your external SSO provider. ## Approach to migrate SSO connections [Section titled “Approach to migrate SSO connections”](#approach-to-migrate-sso-connections) Our main goal is to make sure your current SSO connections keep working seamlessly, while enabling new connections to be set up with Scalekit—giving you the flexibility to migrate to Scalekit whenever you’re ready. This primarily involves two key components: 1. The data migration of tenant resources such as organizations and users. We provide a data migration utility to automate this approach. 2. A SSO proxy service that routes SSO connections between your existing SSO provider and Scalekit. We can assist with a ready-to-deploy SSO proxy service that best suits your infrastructure. Migration assistance available Scalekit offers specialized migration tools to streamline both data migration and SSO proxy configuration. For personalized assistance with your migration plan, [contact our support team](https://docs.scalekit.com/support/contact-us/). ## SSO proxy implementation [Section titled “SSO proxy implementation”](#sso-proxy-implementation) The SSO proxy ensures those connections continue to work while you gradually migrate. This approach is ideal when you prefer a staged rollout—move organizations one by one or all at once with data migration utilty without forcing customers to reconfigure SSO connection settings in their IdP. ### Proxy routes SSO requests to external providers or Scalekit [Section titled “Proxy routes SSO requests to external providers or Scalekit”](#proxy-routes-sso-requests-to-external-providers-or-scalekit) The SSO proxy acts as a smart router that directs authentication requests to the right provider. It sits between your application and both SSO systems, making migration seamless. 1. **Provider selection** Your app sends login requests with user information (email, domain, or organization ID). The proxy analyzes this data and routes authentication to either the external provider or Scalekit. 2. **Redirection to proxy domain** Users are redirected to your proxy domain (e.g., `auth.yourapp.com`) to begin authentication. This domain handles all SSO traffic during migration. 3. **Request forwarding** The proxy forwards authentication requests to the selected provider while preserving all necessary identifiers and session parameters. 4. **Identity provider processing** The user’s IdP processes authentication and sends responses (SAML or OIDC) back to your proxy domain via configured callback URLs. 5. **Response routing** The proxy examines response identifiers to determine which provider handled authentication and routes the callback accordingly. 6. **Code exchange** Your app receives an authorization code with a state indicator showing which provider processed the request. Use this information to complete the authentication flow. ## Set up provider selection in your auth server [Section titled “Set up provider selection in your auth server”](#set-up-provider-selection-in-your-auth-server) 1. **Maintain organization migration mapping** Store information about which organizations are migrated to Scalekit versus those still using external SSO providers. You can use a database, configuration file, or API endpoint based on your app architecture. This mapping determines which SSO provider to use for each organization. example: organization-mapping.js ```javascript 1 const organizationMapping = { 2 'megasoft.com': { provider: 'workos', migrated: false }, 3 'example.com': { provider: 'workos', migrated: false }, 4 'newcompany.com': { provider: 'scalekit', migrated: true } 5 }; ``` 2. **Implement conditional routing logic** Add logic to your authentication endpoint that checks the organization mapping and redirects users to the appropriate SSO provider. For migrated organizations, route to Scalekit; for others, use the external provider. example: auth-server.js ```javascript 1 app.post('/sso-login', (req, res) => { 2 const { email } = req.body; 3 const [, domain] = email.split('@'); 4 5 // Check for force Scalekit header (helpful for debugging) 6 const forceScalekit = req.headers['x-force-sk-route'] === 'yes'; 7 8 if (forceScalekit || organizationMapping[domain]?.migrated) { 9 // Route to Scalekit 10 const authUrl = scalekit.getAuthorizationUrl(redirectUri, { loginHint: email, domain }); 11 res.redirect(authUrl); 12 } else { 13 // Route to external provider 14 const authUrl = externalProvider.getAuthorizationUrl(redirectUri, { email }); 15 res.redirect(authUrl); 16 } 17 }); ``` Debugging tip Add the `x-force-sk-route: yes` header to force requests to Scalekit. This is especially helpful for troubleshooting - customers can use browser extensions like ModHeader to add this header and reproduce flow issues. 3. **SSO proxy handles provider interactions** The SSO proxy manages all interactions with SSO providers and identity providers. See the [SSO proxy architecture overview](#proxy-routes-sso-requests-to-external-providers-or-scalekit) section above for details on how this works. 4. **Create separate callback endpoints** Set up two callback endpoints to handle authorization codes from different providers. While you can use one endpoint, separate endpoints are recommended for clarity and easier debugging. Callback endpoints ```text 1 https://yourapp.com/auth/ext-provider/callback # External provider 2 https://yourapp.com/auth/scalekit/callback # Scalekit ``` 5. **Handle code exchange and user profile retrieval** Your callback endpoints receive authorization codes and exchange them for user profile details. The proxy adds state indicators to help identify which provider processed the authentication. example: callback-handlers.js ```javascript 1 // External provider callback 2 app.get("/auth/ext-provider/callback", async (req, res) => { 3 const { code, state } = req.query; 4 // Exchange code with external provider for user profile 5 const userProfile = await externalProvider.exchangeCode(code); 6 // Create session and redirect 7 }); 8 9 // Scalekit callback 10 app.get("/auth/scalekit/callback", async (req, res) => { 11 const { code } = req.query; 12 // Exchange code with Scalekit for user profile 13 const userProfile = await scalekit.authenticateWithCode(code, redirectUri); 14 // Create session and redirect 15 }); ``` Once you create equivalent organizations in Scalekit for the ones you plan to migrate, the proxy can begin routing callbacks to Scalekit for those organizations while others continue on the external provider. Once you create equivalent organizations in Scalekit for the ones you plan to migrate, the proxy can begin routing callbacks to Scalekit for those organizations while others continue on the external provider. 1. **Update organization mapping for migrated organizations** When organizations are ready for Scalekit, update your mapping to mark them as migrated. The proxy will automatically route these to Scalekit. 2. **Proxy routes Scalekit requests appropriately** The proxy detects migrated organizations and routes authentication to Scalekit while maintaining the same callback URLs for seamless user experience. 3. **Handle Scalekit callbacks** Use your existing Scalekit callback endpoint to process authentication responses and complete the login flow. Note Setting up an SSO proxy can be streamlined based on your infrastructure: * Ready to deploy SSO proxy setup on AWS Lambda * DNS configuration assistance with Cloudflare * Custom infrastructure requirements For any technical assistance with your specific environment or infrastructure needs, please [contact our team](https://docs.scalekit.com/support/contact-us/). We’re here to help ensure a smooth migration process. --- # 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 1 // Extract domain from user's email 2 const email = req.body.email; 3 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 authorization_url = scalekit_client.get_authorization_url( 17 redirect_uri=os.getenv("REDIRECT_URI"), 18 options=AuthorizationUrlOptions(login_hint=email) 19 ) 20 return jsonify({ 21 'ssoAvailable': True, 22 'redirectUrl': authorization_url 23 }) 24 else: 25 # No SSO configured - route to password or social login 26 return jsonify({ 27 'ssoAvailable': False, 28 'message': 'Please use password login' 29 }) 30 except Exception as error: 31 print(f"Failed to check SSO availability: {error}") 32 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 --- # Link to billing, CRM & HR systems > Production-ready patterns for linking Scalekit organizations and users to Stripe, Salesforce, Workday and other enterprise systems using external identifiers External identifiers enable seamless integration between Scalekit and your existing business systems. This guide provides practical patterns for implementing these integrations across common enterprise scenarios including billing platforms, CRM systems, HR systems, and multi-system workflows. ## Integration patterns overview [Section titled “Integration patterns overview”](#integration-patterns-overview) External IDs serve as the bridge between Scalekit’s authentication system and your business infrastructure. Common integration scenarios include: * **Billing and subscription management** - Link customers to payment platforms like Stripe, Chargebee * **Customer relationship management** - Sync with Salesforce, HubSpot, Pipedrive * **Human resources systems** - Connect with Workday, BambooHR, ADP * **Internal tools and databases** - Maintain consistency across custom applications * **Multi-system orchestration** - Coordinate data across multiple platforms ## Billing system integration [Section titled “Billing system integration”](#billing-system-integration) Connect organizations and users with your billing platform to track subscriptions, handle payment events, and maintain customer lifecycle data. ### Stripe integration example [Section titled “Stripe integration example”](#stripe-integration-example) This example shows how to handle subscription updates by finding organizations using external IDs and updating their metadata accordingly. * Node.js Stripe webhook handler ```javascript 1 // When a customer subscribes via Stripe 2 app.post('/stripe/webhook', async (req, res) => { 3 const event = req.body; 4 5 if (event.type === 'customer.subscription.updated') { 6 const customerId = event.data.object.customer; 7 8 // Find organization by external ID (Stripe customer ID) 9 const org = await scalekit.organization.getByExternalId(customerId); 10 11 if (org) { 12 // Update subscription metadata 13 await scalekit.organization.update(org.id, { 14 metadata: { 15 ...org.metadata, 16 subscription_status: event.data.object.status, 17 plan_type: event.data.object.items.data[0].price.lookup_key, 18 last_billing_update: new Date().toISOString(), 19 subscription_current_period_end: new Date(event.data.object.current_period_end * 1000).toISOString() 20 } 21 }); 22 23 // Use case: Automatically provision/deprovision features based on subscription status 24 if (event.data.object.status === 'active') { 25 await enablePremiumFeatures(org.id); 26 } else if (event.data.object.status === 'canceled') { 27 await disablePremiumFeatures(org.id); 28 } 29 } 30 } 31 32 // Handle customer deletion 33 if (event.type === 'customer.deleted') { 34 const customerId = event.data.object.id; 35 const org = await scalekit.organization.getByExternalId(customerId); 36 37 if (org) { 38 await scalekit.organization.update(org.id, { 39 metadata: { 40 ...org.metadata, 41 billing_status: 'deleted', 42 deletion_date: new Date().toISOString() 43 } 44 }); 45 } 46 } 47 48 res.status(200).send('OK'); 49 }); ``` * Python Stripe webhook handler ```python 1 # When a customer subscribes via Stripe 2 @app.route('/stripe/webhook', methods=['POST']) 3 def stripe_webhook(): 4 event = request.json 5 6 if event['type'] == 'customer.subscription.updated': 7 customer_id = event['data']['object']['customer'] 8 9 # Find organization by external ID (Stripe customer ID) 10 org = scalekit.organization.get_by_external_id(customer_id) 11 12 if org: 13 # Update subscription metadata 14 updated_metadata = { 15 **org.metadata, 16 'subscription_status': event['data']['object']['status'], 17 'plan_type': event['data']['object']['items']['data'][0]['price']['lookup_key'], 18 'last_billing_update': datetime.utcnow().isoformat(), 19 'subscription_current_period_end': datetime.fromtimestamp( 20 event['data']['object']['current_period_end'] 21 ).isoformat() 22 } 23 24 scalekit.organization.update(org.id, {'metadata': updated_metadata}) 25 26 # Use case: Automatically provision/deprovision features based on subscription status 27 if event['data']['object']['status'] == 'active': 28 enable_premium_features(org.id) 29 elif event['data']['object']['status'] == 'canceled': 30 disable_premium_features(org.id) 31 32 # Handle customer deletion 33 elif event['type'] == 'customer.deleted': 34 customer_id = event['data']['object']['id'] 35 org = scalekit.organization.get_by_external_id(customer_id) 36 37 if org: 38 updated_metadata = { 39 **org.metadata, 40 'billing_status': 'deleted', 41 'deletion_date': datetime.utcnow().isoformat() 42 } 43 scalekit.organization.update(org.id, {'metadata': updated_metadata}) 44 45 return 'OK', 200 ``` ### Best practices for billing integration [Section titled “Best practices for billing integration”](#best-practices-for-billing-integration) * **Use Stripe customer IDs as external IDs** for organizations to enable quick lookups during webhook processing * **Store subscription metadata** in organization records for immediate access in your application * **Handle subscription lifecycle events** (trial start, subscription active, canceled, past due) * **Implement idempotency** in webhook handlers to prevent duplicate processing * **Use external IDs for user-level billing** when implementing per-seat pricing models ## CRM synchronization [Section titled “CRM synchronization”](#crm-synchronization) Keep organization and user data synchronized between Scalekit and your CRM system to maintain consistent customer records and enable sales team workflows. ### Salesforce integration example [Section titled “Salesforce integration example”](#salesforce-integration-example) * Node.js Salesforce sync integration ```javascript 1 // Sync organization data with Salesforce 2 async function syncOrganizationWithCRM(organizationId, salesforceAccountId) { 3 try { 4 // Fetch account data from Salesforce 5 const crmData = await salesforce.getAccount(salesforceAccountId); 6 7 // Update Scalekit organization with CRM data 8 await scalekit.organization.update(organizationId, { 9 metadata: { 10 salesforce_account_id: salesforceAccountId, 11 industry: crmData.Industry, 12 annual_revenue: crmData.AnnualRevenue, 13 account_owner: crmData.Owner.Name, 14 account_type: crmData.Type, 15 company_size: crmData.NumberOfEmployees, 16 last_crm_sync: new Date().toISOString(), 17 crm_last_modified: crmData.LastModifiedDate 18 } 19 }); 20 21 // Use case: Update user permissions based on account type 22 if (crmData.Type === 'Enterprise') { 23 await enableEnterpriseFeatures(organizationId); 24 } 25 26 } catch (error) { 27 console.error('CRM sync failed:', error); 28 // Log sync failure for monitoring 29 await logSyncFailure('salesforce', organizationId, error); 30 } 31 } 32 33 // Sync user data with Salesforce contacts 34 async function syncUserWithCRM(userId, organizationId, salesforceContactId) { 35 try { 36 const contactData = await salesforce.getContact(salesforceContactId); 37 38 await scalekit.user.updateUser(userId, { 39 metadata: { 40 salesforce_contact_id: salesforceContactId, 41 job_title: contactData.Title, 42 department: contactData.Department, 43 territory: contactData.Sales_Territory__c, 44 last_crm_contact_sync: new Date().toISOString() 45 } 46 }); 47 48 } catch (error) { 49 console.error('User CRM sync failed:', error); 50 } 51 } 52 53 // Bidirectional sync: Update Salesforce when Scalekit data changes 54 async function updateCRMFromScalekit(organizationId) { 55 const org = await scalekit.organization.getById(organizationId); 56 57 if (org.metadata.salesforce_account_id) { 58 await salesforce.updateAccount(org.metadata.salesforce_account_id, { 59 Last_Login_Date__c: new Date().toISOString(), 60 Active_Users__c: await getUserCount(organizationId), 61 Subscription_Status__c: org.metadata.plan_type 62 }); 63 } 64 } ``` * Python Salesforce sync integration ```python 1 # Sync organization data with Salesforce 2 async def sync_organization_with_crm(organization_id, salesforce_account_id): 3 try: 4 # Fetch account data from Salesforce 5 crm_data = await salesforce.get_account(salesforce_account_id) 6 7 # Update Scalekit organization with CRM data 8 metadata = { 9 'salesforce_account_id': salesforce_account_id, 10 'industry': crm_data.get('Industry'), 11 'annual_revenue': crm_data.get('AnnualRevenue'), 12 'account_owner': crm_data.get('Owner', {}).get('Name'), 13 'account_type': crm_data.get('Type'), 14 'company_size': crm_data.get('NumberOfEmployees'), 15 'last_crm_sync': datetime.utcnow().isoformat(), 16 'crm_last_modified': crm_data.get('LastModifiedDate') 17 } 18 19 scalekit.organization.update(organization_id, {'metadata': metadata}) 20 21 # Use case: Update user permissions based on account type 22 if crm_data.get('Type') == 'Enterprise': 23 await enable_enterprise_features(organization_id) 24 25 except Exception as error: 26 print(f'CRM sync failed: {error}') 27 # Log sync failure for monitoring 28 await log_sync_failure('salesforce', organization_id, str(error)) 29 30 # Sync user data with Salesforce contacts 31 async def sync_user_with_crm(user_id, organization_id, salesforce_contact_id): 32 try: 33 contact_data = await salesforce.get_contact(salesforce_contact_id) 34 35 metadata = { 36 'salesforce_contact_id': salesforce_contact_id, 37 'job_title': contact_data.get('Title'), 38 'department': contact_data.get('Department'), 39 'territory': contact_data.get('Sales_Territory__c'), 40 'last_crm_contact_sync': datetime.utcnow().isoformat() 41 } 42 43 scalekit.user.update_user(user_id, {'metadata': metadata}) 44 45 except Exception as error: 46 print(f'User CRM sync failed: {error}') 47 48 # Bidirectional sync: Update Salesforce when Scalekit data changes 49 async def update_crm_from_scalekit(organization_id): 50 org = scalekit.organization.get_by_id(organization_id) 51 52 if org.metadata.get('salesforce_account_id'): 53 await salesforce.update_account(org.metadata['salesforce_account_id'], { 54 'Last_Login_Date__c': datetime.utcnow().isoformat(), 55 'Active_Users__c': await get_user_count(organization_id), 56 'Subscription_Status__c': org.metadata.get('plan_type') 57 }) ``` ### CRM integration best practices [Section titled “CRM integration best practices”](#crm-integration-best-practices) * **Use CRM record IDs as external IDs** to enable quick bidirectional lookups * **Implement scheduled sync jobs** to keep data fresh without overloading APIs * **Handle API rate limits** with exponential backoff and queuing * **Store sync timestamps** to enable incremental updates * **Log sync failures** for monitoring and debugging * **Implement conflict resolution** for bidirectional sync scenarios ## HR system integration [Section titled “HR system integration”](#hr-system-integration) Connect user records with HR systems to automate provisioning, maintain employee data, and handle organizational changes. ### Workday integration pattern [Section titled “Workday integration pattern”](#workday-integration-pattern) HR system integration example ```javascript 1 // Sync user data with HR system during onboarding 2 async function syncNewEmployeeWithScalekit(employeeData) { 3 const { employee_id, email, first_name, last_name, department, start_date, manager_email } = employeeData; 4 5 // Find organization by domain or external ID 6 const domain = email.split('@')[1]; 7 const organization = await scalekit.organization.getByDomain(domain); 8 9 if (organization) { 10 // Create user with HR system external ID 11 const { user } = await scalekit.user.createUserAndMembership(organization.id, { 12 email: email, 13 externalId: employee_id, // HR system employee ID 14 metadata: { 15 hr_employee_id: employee_id, 16 department: department, 17 start_date: start_date, 18 manager_email: manager_email, 19 employee_status: 'active', 20 hr_last_sync: new Date().toISOString() 21 }, 22 userProfile: { 23 firstName: first_name, 24 lastName: last_name 25 }, 26 sendInvitationEmail: true 27 }); 28 29 // Use case: Assign department-based roles 30 await assignDepartmentRoles(user.id, department); 31 32 return user; 33 } 34 } 35 36 // Handle employee status changes 37 async function handleEmployeeStatusChange(employee_id, status) { 38 try { 39 // Find user by HR system external ID 40 const user = await scalekit.user.getUserByExternalId(organization.id, employee_id); 41 42 if (user) { 43 if (status === 'terminated') { 44 // Disable user access 45 await scalekit.user.updateUser(user.id, { 46 metadata: { 47 ...user.metadata, 48 employee_status: 'terminated', 49 termination_date: new Date().toISOString() 50 } 51 }); 52 53 // Remove from organization 54 await scalekit.user.removeMembership(user.id, organization.id); 55 56 } else if (status === 'on_leave') { 57 // Temporarily suspend access 58 await scalekit.user.updateUser(user.id, { 59 metadata: { 60 ...user.metadata, 61 employee_status: 'on_leave', 62 leave_start_date: new Date().toISOString() 63 } 64 }); 65 } 66 } 67 } catch (error) { 68 console.error('HR status sync failed:', error); 69 } 70 } ``` ## Multi-system integration workflows [Section titled “Multi-system integration workflows”](#multi-system-integration-workflows) Orchestrate data across multiple systems using external IDs as the common identifier thread. ### Customer lifecycle automation [Section titled “Customer lifecycle automation”](#customer-lifecycle-automation) Multi-system workflow example ```javascript 1 // Complete customer onboarding workflow 2 async function onboardNewCustomer(customerData) { 3 const { company_name, admin_email, plan_type, salesforce_account_id, stripe_customer_id } = customerData; 4 5 try { 6 // 1. Create organization in Scalekit 7 const organization = await scalekit.organization.create({ 8 display_name: company_name, 9 external_id: stripe_customer_id, // Use billing system ID as primary external ID 10 metadata: { 11 plan_type: plan_type, 12 salesforce_account_id: salesforce_account_id, 13 stripe_customer_id: stripe_customer_id, 14 onboarding_status: 'pending', 15 created_date: new Date().toISOString() 16 } 17 }); 18 19 // 2. Create admin user 20 const { user } = await scalekit.user.createUserAndMembership(organization.id, { 21 email: admin_email, 22 externalId: `${stripe_customer_id}_admin`, // Composite external ID 23 metadata: { 24 role_type: 'admin', 25 onboarding_step: 'account_created' 26 }, 27 sendInvitationEmail: true 28 }); 29 30 // 3. Update CRM with Scalekit IDs 31 await salesforce.updateAccount(salesforce_account_id, { 32 Scalekit_Organization_ID__c: organization.id, 33 Scalekit_Admin_User_ID__c: user.id, 34 Onboarding_Status__c: 'In Progress' 35 }); 36 37 // 4. Configure billing in Stripe 38 await stripe.customers.update(stripe_customer_id, { 39 metadata: { 40 scalekit_org_id: organization.id, 41 scalekit_admin_user_id: user.id 42 } 43 }); 44 45 // 5. Send onboarding notifications 46 await sendOnboardingEmail(admin_email, organization.id); 47 await notifySalesTeam(salesforce_account_id, 'customer_onboarded'); 48 49 return { organization, user }; 50 51 } catch (error) { 52 console.error('Customer onboarding failed:', error); 53 // Rollback logic here 54 throw error; 55 } 56 } ``` ## Error handling and retry patterns [Section titled “Error handling and retry patterns”](#error-handling-and-retry-patterns) Implement robust error handling for external system integrations to ensure data consistency and reliability. ### Retry with exponential backoff [Section titled “Retry with exponential backoff”](#retry-with-exponential-backoff) Robust integration error handling ```javascript 1 // Utility function for retrying API calls with exponential backoff 2 async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) { 3 for (let attempt = 1; attempt <= maxRetries; attempt++) { 4 try { 5 return await fn(); 6 } catch (error) { 7 if (attempt === maxRetries) { 8 throw error; 9 } 10 11 // Exponential backoff with jitter 12 const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000; 13 await new Promise(resolve => setTimeout(resolve, delay)); 14 } 15 } 16 } 17 18 // Resilient external ID lookup 19 async function findOrganizationWithRetry(externalId) { 20 return retryWithBackoff(async () => { 21 const org = await scalekit.organization.getByExternalId(externalId); 22 if (!org) { 23 throw new Error(`Organization not found for external ID: ${externalId}`); 24 } 25 return org; 26 }); 27 } 28 29 // Webhook processing with error handling 30 app.post('/webhook', async (req, res) => { 31 try { 32 const { external_id, event_type, data } = req.body; 33 34 // Find organization with retry logic 35 const organization = await findOrganizationWithRetry(external_id); 36 37 // Process the webhook data 38 await processWebhookEvent(organization, event_type, data); 39 40 res.status(200).json({ status: 'success' }); 41 42 } catch (error) { 43 console.error('Webhook processing failed:', error); 44 45 // Queue for retry if it's a temporary failure 46 if (isRetryableError(error)) { 47 await queueWebhookForRetry(req.body); 48 res.status(202).json({ status: 'queued_for_retry' }); 49 } else { 50 res.status(400).json({ status: 'error', message: error.message }); 51 } 52 } 53 }); 54 55 function isRetryableError(error) { 56 return error.code === 'NETWORK_ERROR' || 57 error.code === 'RATE_LIMITED' || 58 error.status >= 500; 59 } ``` ## Security considerations [Section titled “Security considerations”](#security-considerations) When implementing external ID integrations, follow these security best practices: ### Webhook security [Section titled “Webhook security”](#webhook-security) Secure webhook handling ```javascript 1 // Verify webhook signatures 2 function verifyWebhookSignature(payload, signature, secret) { 3 const expectedSignature = crypto 4 .createHmac('sha256', secret) 5 .update(payload) 6 .digest('hex'); 7 8 return crypto.timingSafeEqual( 9 Buffer.from(signature, 'hex'), 10 Buffer.from(expectedSignature, 'hex') 11 ); 12 } 13 14 // Rate limiting for webhook endpoints 15 const webhookLimiter = rateLimit({ 16 windowMs: 1 * 60 * 1000, // 1 minute 17 max: 100, // limit each IP to 100 requests per windowMs 18 message: 'Too many webhook requests from this IP' 19 }); 20 21 app.post('/webhook', webhookLimiter, (req, res) => { 22 // Verify signature before processing 23 if (!verifyWebhookSignature(req.body, req.headers['x-signature'], process.env.WEBHOOK_SECRET)) { 24 return res.status(401).json({ error: 'Invalid signature' }); 25 } 26 27 // Process webhook... 28 }); ``` ### Data validation and sanitization [Section titled “Data validation and sanitization”](#data-validation-and-sanitization) * **Validate external IDs** before using them in database queries * **Sanitize metadata** to prevent injection attacks * **Use prepared statements** for database operations * **Implement input validation** for all external data * **Log security events** for monitoring and auditing Tip External IDs and metadata are included in JWT tokens when users authenticate, making this information immediately available in your application without additional API calls. This enables real-time feature toggles and personalization based on external system data. ## Monitoring and observability [Section titled “Monitoring and observability”](#monitoring-and-observability) Implement comprehensive monitoring for external ID integrations to ensure system health and quick issue resolution. ### Integration health monitoring [Section titled “Integration health monitoring”](#integration-health-monitoring) Integration monitoring example ```javascript 1 // Track integration health metrics 2 class IntegrationMonitor { 3 constructor() { 4 this.metrics = { 5 successful_syncs: 0, 6 failed_syncs: 0, 7 average_sync_time: 0, 8 last_successful_sync: null 9 }; 10 } 11 12 async recordSyncAttempt(system, success, duration) { 13 if (success) { 14 this.metrics.successful_syncs++; 15 this.metrics.last_successful_sync = new Date(); 16 } else { 17 this.metrics.failed_syncs++; 18 } 19 20 // Update average sync time 21 this.updateAverageSyncTime(duration); 22 23 // Send metrics to monitoring system 24 await this.sendMetrics(system, this.metrics); 25 } 26 27 updateAverageSyncTime(duration) { 28 const totalSyncs = this.metrics.successful_syncs + this.metrics.failed_syncs; 29 this.metrics.average_sync_time = 30 (this.metrics.average_sync_time * (totalSyncs - 1) + duration) / totalSyncs; 31 } 32 } 33 34 // Usage in integration functions 35 const monitor = new IntegrationMonitor(); 36 37 async function syncWithExternalSystem(externalId, data) { 38 const startTime = Date.now(); 39 let success = false; 40 41 try { 42 await performSync(externalId, data); 43 success = true; 44 } catch (error) { 45 console.error('Sync failed:', error); 46 throw error; 47 } finally { 48 const duration = Date.now() - startTime; 49 await monitor.recordSyncAttempt('external_system', success, duration); 50 } 51 } ``` ## Best practices summary [Section titled “Best practices summary”](#best-practices-summary) ### External ID management [Section titled “External ID management”](#external-id-management) * **Use meaningful, stable identifiers** from your primary business system * **Implement consistent naming conventions** across all external IDs * **Handle ID migration scenarios** when external systems change * **Validate external IDs** before using them in operations ### Integration reliability [Section titled “Integration reliability”](#integration-reliability) * **Implement retry logic** with exponential backoff for API calls * **Use webhooks for real-time sync** and scheduled jobs for periodic reconciliation * **Handle rate limits** gracefully with queuing and backoff strategies * **Monitor integration health** with comprehensive metrics and alerting ### Security and compliance [Section titled “Security and compliance”](#security-and-compliance) * **Verify webhook signatures** to ensure authenticity * **Implement rate limiting** on webhook endpoints * **Validate and sanitize** all external data * **Audit integration activities** for compliance requirements ### Performance optimization [Section titled “Performance optimization”](#performance-optimization) * **Cache frequently accessed external ID mappings** * **Batch operations** where possible to reduce API calls * **Use appropriate timeouts** for external API calls * **Implement circuit breakers** for unreliable external services This integration approach enables seamless data flow between Scalekit and your business systems while maintaining security, reliability, and performance standards. --- # DOCUMENT BOUNDARY --- # Modular social logins > Learn how to integrate modular social logins module with Scalekit Social login enables authentication through existing accounts from providers like Google, Microsoft, and GitHub. Users don’t need to create or remember new credentials, making the sign-in process faster and more convenient. This guide explains how to implement social login in your application with Scalekit’s OAuth 2.0 integration. ![How Scalekit works](/.netlify/images?url=_astro%2F0.CtcbvoxC.png\&w=5776\&h=1924\&dpl=69cce21a4f77360008b1503a) 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` Follow the [installation guide](/authenticate/set-up-scalekit/) to configure Scalekit in your application. Go to Dashboard > Authentication > General to **turn off the Full-Stack Auth** since you’d use the modular social logins module. This disables user management and session management features and let’s to only use social login authentication. 2. ## 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 [Set up →](/guides/integrations/social-connections/google) ### GitHub Allow users to authenticate using their GitHub credentials [Set up →](/guides/integrations/social-connections/github) ### Microsoft Integrate Microsoft accounts for seamless user authentication [Set up →](/guides/integrations/social-connections/microsoft) ### GitLab Enable GitLab-based authentication for your application [Set up →](/guides/integrations/social-connections/gitlab) ### LinkedIn Let users sign in with their LinkedIn accounts using OAuth 2.0 [Set up →](/guides/integrations/social-connections/linkedin) ### Salesforce Enable Salesforce-based authentication for your application [Set up →](/guides/integrations/social-connections/salesforce) After configuration, Scalekit can interact with these providers to authenticate users and verify their identities. 3. ## From your application, redirect users to provider’s OAuth pages [Section titled “From your application, redirect users to provider’s OAuth pages”](#from-your-application-redirect-users-to-providers-oauth-pages) Create an authorization URL to redirect users to social provider’s sign-in page. Use the Scalekit SDK to construct this URL with your redirect URI and provider identifier. Supported `provider` values: `google`, `microsoft`, `github`, `salesforce`, `linkedin`, `gitlab` * Node.js ```javascript 1 // 2 const authorizationURL = scalekit.getAuthorizationUrl(redirectUri, { 3 provider: 'google', 4 state: state, // recommended 5 }); 6 7 /* 8 https://auth.scalekit.com/authorize? 9 client_id=skc_122056050118122349527& 10 redirect_uri=https://yourapp.com/auth/callback& 11 provider=google 12 */ ``` * Python ```python 1 options = AuthorizationUrlOptions() 2 3 options.provider = 'google' 4 5 authorization_url = scalekit_client.get_authorization_url( 6 redirect_uri=, 7 options=options 8 ) ``` * Go ```go 1 options := scalekitClient.AuthorizationUrlOptions{} 2 // Pass the social login provider details while constructing the authorization URL. 3 options.Provider = "google" 4 5 authorizationURL := scalekitClient.GetAuthorizationUrl( 6 redirectUrl, 7 options, 8 ) 9 // Next step is to redirect the user to this authorization URL 10 } ``` * Java ```java 1 package com.scalekit; 2 3 import com.scalekit.internal.http.AuthorizationUrlOptions; 4 5 public class Main { 6 7 public static void main(String[] args) { 8 ScalekitClient scalekitClient = new ScalekitClient( 9 "", 10 "", 11 "" 12 ); 13 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 14 options.setProvider("google"); 15 try { 16 // Pass the social login provider details while constructing the authorization URL. 17 String url = scalekitClient.authentication().getAuthorizationUrl(redirectUrl, options).toString(); 18 } catch (Exception e) { 19 System.out.println(e.getMessage()); 20 } 21 } 22 } ``` After the user successfully authenticates with the selected social login provider, they will be redirected back to your application. Scalekit passes an authorization `code` to your registered callback endpoint, which you’ll use in the next step to retrieve user information. 4. ## 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. 1. Add a callback endpoint in your application (typically `https://your-app.com/auth/callback`) 2. [Register](/guides/dashboard/allowed-callback-url/) 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 and proceed to creating session and logging in the user. * Node.js ```javascript 1 const { code, state, error, error_description } = req.query; 2 3 if (error) { 4 // Handle errors (use error_description if present) 5 } 6 7 const authResult = await scalekit.authenticateWithCode(code, redirectUri); 8 9 // authResult.user has the authenticated user's details 10 const userEmail = authResult.user.email; 11 12 // Next step: create a session for this user and allow access ``` * Python ```python 1 code = request.args.get('code') 2 error = request.args.get('error') 3 error_description = request.args.get('error_description') 4 5 if error: 6 raise Exception(error_description) 7 8 auth_result = scalekit_client.authenticate_with_code( 9 code, 10 11 ) 12 13 # result.user has the authenticated user's details 14 user_email = auth_result.user.email 15 16 # Next step: create a session for this user and allow access ``` * Go ```go 1 code := r.URL.Query().Get("code") 2 error := r.URL.Query().Get("error") 3 errorDescription := r.URL.Query().Get("error_description") 4 5 if error != "" { 6 // Handle errors and exit 7 } 8 9 authResult, err := scalekitClient.AuthenticateWithCode(r.Context(), code, redirectUrl) 10 if err != nil { 11 // Handle errors and exit 12 } 13 14 // authResult.User has the authenticated user's details 15 userEmail := authResult.User.Email 16 17 // Next step: create a session for this user and allow access ``` * Java ```java 1 String code = request.getParameter("code"); 2 String error = request.getParameter("error"); 3 String errorDescription = request.getParameter("error_description"); 4 if (error != null && !error.isEmpty()) { 5 // Handle errors 6 return; 7 } 8 try { 9 AuthenticationResponse res = scalekitClient.authentication().authenticateWithCode(code, redirectUrl); 10 // res.getIdTokenClaims() has the authenticated user's details 11 String userEmail = res.getIdTokenClaims().getEmail(); 12 13 } catch (Exception e) { 14 // Handle errors 15 } 16 17 // Next step: create a session for this user and allow access ``` The *auth result* object * Auth result ```js { user: { email: "john.doe@example.com" // User's email // any additional common fields }, idToken: "", // JWT with user profile claims accessToken: "", // JWT for API calls expiresIn: 899 // Seconds until expiration } ``` * Decoded ID token (JWT) ```json { "alg": "RS256", "kid": "snk_82937465019283746", "typ": "JWT" }.{ "amr": [ "conn_92847563920187364" ], "at_hash": "j8kqPm3nRt5Kx2Vy9wL_Zp", "aud": [ "skc_73645291837465928" ], "azp": "skc_73645291837465928", "c_hash": "Hy4k2M9pWnX7vqR8_Jt3bg", "client_id": "skc_73645291837465928", "email": "alice.smith@example.com", "email_verified": true, "exp": 1751697469, "iat": 1751438269, "iss": "https://demo-company-dev.scalekit.cloud", "sid": "ses_83746592018273645", "sub": "conn_92847563920187364;alice.smith@example.com" // A scalekit user ID is sent if user management is enabled }.[Signature] ``` * Decoded access token ```json { "alg": "RS256", "kid": "snk_794467716206433", "typ": "JWT" }.{ "iss": "https://acme-corp-dev.scalekit.cloud", "sub": "conn_794467724427269;robert.wilson@acme.com", "aud": [ "skc_794467724259497" ], "exp": 1751439169, "iat": 1751438269, "nbf": 1751438269, "client_id": "skc_794467724259497", "jti": "tkn_794754665320942", // External identifiers if updated on Scalekit "xoid": "ext_org_123", // Organization ID "xuid": "ext_usr_456" // User ID }.[Signature] ``` Your application now supports social login authentication. Users can sign in securely using their preferred social identity providers like Google, GitHub, Microsoft, and more. --- # DOCUMENT BOUNDARY --- # Preserve target route post-auth > Redirect users back to page they asked for after authentication using a signed return URL Users may bookmark specific pages of your app, but their session might be expired. They need to be redirected to the page they asked for after authentication. That means your app needs to preserve the user’s original destination. You will capture the user’s original destination, carry it through the OAuth flow safely, and redirect back after login. You will prevent open-redirect attacks by validating and signing the return URL. Two safe patterns Use either `state` embedding (short paths only) or a signed `return_to` cookie. Avoid passing raw URLs in query strings without validation. 1. ## Capture the intended destination [Section titled “Capture the intended destination”](#capture-the-intended-destination) When an unauthenticated user requests a protected route, capture its path. * Node.js Express.js ```javascript 1 app.get('/login', (req, res) => { 2 const nextPath = typeof req.query.next === 'string' ? req.query.next : '/' 3 // Only allow internal paths 4 const safe = nextPath.startsWith('/') && !nextPath.startsWith('//') ? nextPath : '/' 5 res.cookie('sk_return_to', safe, { httpOnly: true, secure: true, sameSite: 'lax', path: '/' }) 6 // build authorization URL next 7 }) ``` * Python Flask ```python 1 @app.route('/login') 2 def login(): 3 next_path = request.args.get('next', '/') 4 safe = next_path if next_path.startswith('/') and not next_path.startswith('//') else '/' 5 resp = make_response() 6 resp.set_cookie('sk_return_to', safe, httponly=True, secure=True, samesite='Lax', path='/') 7 return resp ``` * Go Gin ```go 1 func login(c *gin.Context) { 2 nextPath := c.Query("next") 3 if nextPath == "" || !strings.HasPrefix(nextPath, "/") || strings.HasPrefix(nextPath, "//") { 4 nextPath = "/" 5 } 6 cookie := &http.Cookie{Name: "sk_return_to", Value: nextPath, HttpOnly: true, Secure: true, Path: "/"} 7 http.SetCookie(c.Writer, cookie) 8 } ``` * Java Spring ```java 1 @GetMapping("/login") 2 public void login(HttpServletRequest request, HttpServletResponse response) { 3 String nextPath = Optional.ofNullable(request.getParameter("next")).orElse("/"); 4 boolean safe = nextPath.startsWith("/") && !nextPath.startsWith("//"); 5 Cookie cookie = new Cookie("sk_return_to", safe ? nextPath : "/"); 6 cookie.setHttpOnly(true); cookie.setSecure(true); cookie.setPath("/"); 7 response.addCookie(cookie); 8 } ``` Reading cookies in Express If you access `req.cookies` in Node.js, enable cookie parsing middleware (for example, `cookie-parser`) early in your server setup. 2. ## Build the authorization URL [Section titled “Build the authorization URL”](#build-the-authorization-url) Generate the authorization URL as in the quickstart. Optionally include a short hint in `state` like `"n=/billing"` after signing or encoding. * Node.js Express.js ```javascript 1 const redirectUri = 'https://your-app.com/auth/callback' 2 const options = { scopes: ['openid','profile','email','offline_access'] } 3 const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options) 4 res.redirect(authorizationUrl) ``` * Python Flask ```python 1 redirect_uri = 'https://your-app.com/auth/callback' 2 options = AuthorizationUrlOptions(scopes=['openid','profile','email','offline_access']) 3 authorization_url = scalekit_client.get_authorization_url(redirect_uri, options) 4 return redirect(authorization_url) ``` * Go Gin ```go 1 redirectUri := "https://your-app.com/auth/callback" 2 options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}} 3 authorizationURL, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options) 4 c.Redirect(http.StatusFound, authorizationURL.String()) ``` * Java Spring ```java 1 String redirectUri = "https://your-app.com/auth/callback"; 2 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 3 options.setScopes(Arrays.asList("openid","profile","email","offline_access")); 4 URL authorizationUrl = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options); 5 return new RedirectView(authorizationUrl.toString()); ``` 3. ## After callback, redirect safely [Section titled “After callback, redirect safely”](#after-callback-redirect-safely) After exchanging the code and creating a session, read `sk_return_to`. Validate and normalize the path. Default to `/dashboard` or `/`. * Node.js Express.js ```javascript 1 app.get('/auth/callback', async (req, res) => { 2 // ... exchange code ... 3 const raw = req.cookies.sk_return_to || '/' 4 const safe = raw.startsWith('/') && !raw.startsWith('//') ? raw : '/' 5 res.clearCookie('sk_return_to', { path: '/' }) 6 res.redirect(safe || '/dashboard') 7 }) ``` * Python Flask ```python 1 def callback(): 2 # ... exchange code ... 3 raw = request.cookies.get('sk_return_to', '/') 4 safe = raw if raw.startswith('/') and not raw.startswith('//') else '/' 5 resp = redirect(safe or '/dashboard') 6 resp.delete_cookie('sk_return_to', path='/') 7 return resp ``` * Go Gin ```go 1 func callback(c *gin.Context) { 2 // ... exchange code ... 3 raw, _ := c.Cookie("sk_return_to") 4 if raw == "" || !strings.HasPrefix(raw, "/") || strings.HasPrefix(raw, "//") { 5 raw = "/" 6 } 7 http.SetCookie(c.Writer, &http.Cookie{Name: "sk_return_to", Value: "", MaxAge: -1, Path: "/"}) 8 c.Redirect(http.StatusFound, raw) 9 } ``` * Java Spring ```java 1 public RedirectView callback(HttpServletRequest request, HttpServletResponse response) { 2 // ... exchange code ... 3 String raw = getCookie(request, "sk_return_to").orElse("/"); 4 boolean ok = raw.startsWith("/") && !raw.startsWith("//"); 5 Cookie clear = new Cookie("sk_return_to", ""); clear.setPath("/"); clear.setMaxAge(0); 6 response.addCookie(clear); 7 return new RedirectView(ok ? raw : "/dashboard"); 8 } ``` 4. ## Sign return\_to values Optional [Section titled “Sign return\_to values ”](#sign-return_to-values) If you pass `return_to` via query string or store longer values, compute an HMAC and verify it before redirecting. Reject unsigned or invalid pairs. * Node.js HMAC signing ```javascript 1 import crypto from 'crypto' 2 function sign(value, secret) { 3 const mac = crypto.createHmac('sha256', secret).update(value).digest('base64url') 4 return `${value}|${mac}` 5 } 6 function verify(signed, secret) { 7 const [v, mac] = signed.split('|') 8 const good = crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(sign(v, secret).split('|')[1])) 9 return good ? v : null 10 } ``` * Python HMAC signing ```python 1 import hmac, hashlib, base64 2 def sign(value: str, secret: bytes) -> str: 3 mac = hmac.new(secret, value.encode(), hashlib.sha256).digest() 4 return f"{value}|{base64.urlsafe_b64encode(mac).decode().rstrip('=')}" 5 def verify(signed: str, secret: bytes) -> str | None: 6 try: 7 value, mac = signed.split('|', 1) 8 expected = sign(value, secret).split('|', 1)[1] 9 if hmac.compare_digest(mac, expected): 10 return value 11 except Exception: 12 pass 13 return None ``` * Go HMAC signing ```go 1 import ( 2 "crypto/hmac" 3 "crypto/sha256" 4 "encoding/base64" 5 ) 6 func sign(value string, secret []byte) string { 7 mac := hmac.New(sha256.New, secret) 8 mac.Write([]byte(value)) 9 sum := mac.Sum(nil) 10 return value + "|" + base64.RawURLEncoding.EncodeToString(sum) 11 } 12 func verify(signed string, secret []byte) *string { 13 parts := strings.SplitN(signed, "|", 2) 14 if len(parts) != 2 { return nil } 15 expected := strings.SplitN(sign(parts[0], secret), "|", 2)[1] 16 if hmac.Equal([]byte(parts[1]), []byte(expected)) { 17 return &parts[0] 18 } 19 return nil 20 } ``` * Java HMAC signing ```java 1 import javax.crypto.Mac; 2 import javax.crypto.spec.SecretKeySpec; 3 import java.util.Base64; 4 String sign(String value, byte[] secret) throws Exception { 5 Mac mac = Mac.getInstance("HmacSHA256"); 6 mac.init(new SecretKeySpec(secret, "HmacSHA256")); 7 byte[] raw = mac.doFinal(value.getBytes(StandardCharsets.UTF_8)); 8 String b64 = Base64.getUrlEncoder().withoutPadding().encodeToString(raw); 9 return value + "|" + b64; 10 } 11 String verify(String signed, byte[] secret) throws Exception { 12 String[] parts = signed.split("\\|", 2); 13 if (parts.length != 2) return null; 14 String expected = sign(parts[0], secret).split("\\|", 2)[1]; 15 return MessageDigest.isEqual(parts[1].getBytes(StandardCharsets.UTF_8), expected.getBytes(StandardCharsets.UTF_8)) ? parts[0] : null; 16 } ``` Limit scope and length Allowlist a small set of internal prefixes (for example, `/app`, `/billing`) and cap `return_to` length (for example, 512 chars). Reject anything else. 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 redirects. --- # 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. To ensure the request is coming from Scalekit and not a malicious actor, you should verify the request using the signing secret found in the Scalekit dashboard > Webhook > *Any Endpoint*. Here’s an example of how to verify webhooks using the Svix library: * Node.js ```javascript 1 app.post('/webhook', async (req, res) => { 2 // Parse the JSON body of the request 3 const event = await req.json(); 4 5 // Get headers from the request 6 const headers = req.headers; 7 8 // Secret from Scalekit dashboard > Webhooks 9 const secret = process.env.SCALEKIT_WEBHOOK_SECRET; 10 11 try { 12 // Verify the webhook payload 13 await scalekit.verifyWebhookPayload(secret, headers, event); 14 } catch (error) { 15 return res.status(400).json({ 16 error: 'Invalid signature', 17 }); 18 } 19 }); ``` * 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 # Get request data 8 body = await request.body() 9 10 # Extract webhook headers 11 headers = { 12 'webhook-id': request.headers.get('webhook-id'), 13 'webhook-signature': request.headers.get('webhook-signature'), 14 'webhook-timestamp': request.headers.get('webhook-timestamp') 15 } 16 17 # Verify webhook signature 18 is_valid = scalekit.verify_webhook_payload( 19 secret='', 20 headers=headers, 21 payload=body 22 ) 23 print(is_valid) 24 25 return JSONResponse( 26 status_code=201, 27 content='' 28 ) ``` * 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 // Verify webhook signature 19 _, err = sc.VerifyWebhookPayload( 20 webhookSecret, 21 headers, 22 bodyBytes 23 ) 24 if err != nil { 25 http.Error(w, err.Error(), http.StatusUnauthorized) 26 return 27 } 28 }) ``` * Java ```java 1 @PostMapping("/webhook") 2 public String webhook(@RequestBody String body, @RequestHeader Map headers) { 3 String secret = ""; 4 5 // Verify webhook signature 6 boolean valid = scalekit.webhook().verifyWebhookPayload(secret, headers, body.getBytes()); 7 8 if (!valid) { 9 return "error"; 10 } 11 12 ObjectMapper mapper = new ObjectMapper(); 13 14 try { 15 // Parse event data 16 JsonNode node = mapper.readTree(body); 17 String eventType = node.get("type").asText(); 18 JsonNode data = node.get("data"); 19 20 // Handle different event types 21 switch (eventType) { 22 case "organization.directory.user_created": 23 handleUserCreate(data); 24 break; 25 case "organization.directory.user_updated": 26 handleUserUpdate(data); 27 break; 28 default: 29 System.out.println("Unhandled event type: " + eventType); 30 } 31 } catch (IOException e) { 32 return "error"; 33 } 34 35 return "ok"; 36 } ``` ## 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 String webhook(@RequestBody String body, @RequestHeader Map headers) { 3 // Verify webhook signature first 4 String secret = ""; 5 if (!verifyWebhookSignature(secret, headers, body)) { 6 return "error"; 7 } 8 9 try { 10 // Parse event data 11 ObjectMapper mapper = new ObjectMapper(); 12 JsonNode node = mapper.readTree(body); 13 String eventType = node.get("type").asText(); 14 JsonNode data = node.get("data"); 15 16 // Handle different event types 17 switch (eventType) { 18 case "organization.directory.user_created": 19 handleUserCreate(data); 20 break; 21 case "organization.directory.user_updated": 22 handleUserUpdate(data); 23 break; 24 default: 25 System.out.println("Unhandled event type: " + eventType); 26 } 27 } catch (IOException e) { 28 return "error"; 29 } 30 31 return "ok"; 32 } ``` ## 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. By following these best practices, you can ensure that your application effectively handles events from Scalekit, maintaining optimal performance and security. ## 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. ## Advanced signature verification [Section titled “Advanced signature verification”](#advanced-signature-verification) While using the Scalekit SDK is recommended for webhook signature verification, you can also verify signatures manually using HMAC-SHA256 libraries when the SDK isn’t available for your language. ### Manual signature verification [Section titled “Manual signature verification”](#manual-signature-verification) Manual signature verification ```javascript 1 function verifySignatureManually(rawBody, signature, secret) { 2 const crypto = require('crypto'); 3 4 // Extract timestamp and signature from header 5 // Header format: "t=,v1=" 6 const elements = signature.split(','); 7 const timestamp = elements.find(el => el.startsWith('t=')).substring(2); 8 const receivedSignature = elements.find(el => el.startsWith('v1=')).substring(3); 9 10 // Create expected signature 11 // Payload format: . 12 const payload = `${timestamp}.${rawBody}`; 13 const expectedSignature = crypto 14 .createHmac('sha256', secret) 15 .update(payload, 'utf8') 16 .digest('hex'); 17 18 // Compare signatures securely using timing-safe comparison 19 // This prevents timing attacks 20 return crypto.timingSafeEqual( 21 Buffer.from(receivedSignature, 'hex'), 22 Buffer.from(expectedSignature, 'hex') 23 ); 24 } ``` ### Timestamp validation [Section titled “Timestamp validation”](#timestamp-validation) Always validate the webhook timestamp to prevent replay attacks: Timestamp validation ```javascript 1 function validateWebhookTimestamp(timestamp, toleranceSeconds = 300) { 2 // Convert timestamp to milliseconds 3 const webhookTime = parseInt(timestamp) * 1000; 4 const currentTime = Date.now(); 5 const timeDifference = Math.abs(currentTime - webhookTime); 6 7 // Reject webhooks older than tolerance period (default 5 minutes) 8 if (timeDifference > toleranceSeconds * 1000) { 9 throw new Error('Webhook timestamp too old or too far in future'); 10 } 11 12 return true; 13 } ``` ## 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 } 27 28 // Mock webhook signature for testing 29 function createTestSignature(payload, secret) { 30 const crypto = require('crypto'); 31 const timestamp = Math.floor(Date.now() / 1000); 32 const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload); 33 const signature = crypto 34 .createHmac('sha256', secret) 35 .update(`${timestamp}.${payloadString}`) 36 .digest('hex'); 37 38 return { 39 'webhook-id': 'evt_test_' + Date.now(), 40 'webhook-timestamp': timestamp.toString(), 41 'webhook-signature': `t=${timestamp},v1=${signature}` 42 }; 43 } 44 45 // Integration test 46 async function testWebhookIntegration() { 47 const testSecret = 'test_secret_key'; 48 const testEvent = { 49 type: 'organization.directory.user_created', 50 data: { /* test data */ } 51 }; 52 53 const headers = createTestSignature(testEvent, testSecret); 54 55 // Make request to your webhook endpoint 56 const response = await fetch('http://localhost:3000/webhooks/manage-users', { 57 method: 'POST', 58 headers: { 59 'Content-Type': 'application/json', 60 ...headers 61 }, 62 body: JSON.stringify(testEvent) 63 }); 64 65 assert(response.status === 201, 'Expected 201 status'); 66 console.log('Integration test passed'); 67 } ``` ## 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. ### 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 1 pip install scalekit-sdk-python ``` send\_user\_details.py ```python 1 from scalekit import ScalekitClient 2 import os 3 4 scalekit = 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 scalekit.auth.update_login_user_details( 12 connection_id="{{connection_id}}", 13 login_request_id="{{login_request_id}}", 14 user={ 15 "sub": "1234567890", 16 "email": "alice@example.com" 17 }, 18 ) ``` * Node.js ```bash 1 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 await scalekit.auth.updateLoginUserDetails( 12 '{{connection_id}}', // connectionId 13 '{{login_request_id}}', // loginRequestId 14 { 15 sub: '1234567890', 16 email: 'alice@example.com' 17 } 18 ); ``` * Go ```bash 1 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 skClient := scalekit.NewScalekitClient( 13 os.Getenv("SCALEKIT_ENVIRONMENT_URL"), 14 os.Getenv("SCALEKIT_CLIENT_ID"), 15 os.Getenv("SCALEKIT_CLIENT_SECRET"), 16 ) 17 err := skClient.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 // Only if there is no error, perform the redirect to scalekit using the redirect url on your Scalekit Dashboard -> MCP Servers 29 return nil 30 } ``` * 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* Terminal ```bash 1 curl --location '{{env_url}}/oauth/token' \ 2 --header 'Content-Type: application/x-www-form-urlencoded' \ 3 --data-urlencode 'grant_type=client_credentials' \ 4 --data-urlencode 'client_id={{sk_client_id}}' \ 5 --data-urlencode 'client_secret={{sk_client_secret}}' ``` Scalekit responds with a JSON payload similar to: ```json 1 { 2 "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIn0...", 3 "token_type": "Bearer", 4 "expires_in": 3600 5 } ``` Use the `access_token` in the `Authorization` header when making a machine-to-machine POST request to Scalekit with the user’s details. ```bash 1 curl --location '{{env_url}}/api/v1/connections/{{connection_id}}/auth-requests/{{login_request_id}}/user' \ 2 --header 'Content-Type: application/json' \ 3 --header 'Authorization: Bearer {{access_token}}' \ 4 --data-raw '{ 5 "sub": "1234567890", 6 "email": "alice@example.com" 7 }' ``` 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. You can get the `{{connection_id}}` from the User Info Post URL from Scalekit MCP server dashboard. If the user info post url is `https://yourapp.scalekit.com/api/v1/connections/conn_1234567890/auth-requests/{{login_request_id}}/user`, your connection\_id is `conn_1234567890`. *** ### 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. **Download our sample MCP Server:** We have put together a simple MCP server that you can check out and run it locally to test the end to end functionality of a working MCP server complete with authentication and authorization. You can download and execute a sample MCP server implementation from [GitHub](https://github.com/scalekit-inc/mcp-auth-demos). **Try out the BYOA MCP server**: Scalekit provides a demo MCP server that shows how to implement your own auth integration. Clone the [BYOA MCP server](https://github.com/scalekit-inc/byoa-demo-mcp) to test end-to-end authentication in your environment. --- # 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.** ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) [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, and Salesforce. 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=69cce21a4f77360008b1503a) 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 --- # Passwordless OIDC Quickstart > Add passwordless sign-in with OTP or magic link via OIDC This guide shows you how to implement passwordless authentication with Scalekit over OIDC protocol. Users verify with a email verification code (OTP) or a magic link or both. Review the authentication sequence ### Build with a coding agent * Claude Code ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` ```bash /plugin install full-stack-auth@scalekit-auth-stack ``` * GitHub Copilot CLI ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` ```bash copilot plugin install full-stack-auth@scalekit-auth-stack ``` * 40+ agents ```bash npx skills add scalekit-inc/skills --skill implementing-scalekit-fsa ``` [Continue building with AI →](/dev-kit/build-with-ai/full-stack-auth/) 1. ## Set up Scalekit and register a callback endpoint [Section titled “Set up Scalekit and register a callback endpoint”](#set-up-scalekit-and-register-a-callback-endpoint) Follow the [installation guide](/authenticate/set-up-scalekit/) to configure Scalekit in your application. Scalekit verifies user identities and creates sessions. After successful verification, Scalekit creates a user record and sends the user information to your callback endpoint. **Create a callback endpoint:** 1. Add a callback endpoint to your application (typically `https://your-app.com/auth/callback`) 2. Register this URL in your Scalekit dashboard Learn more about [callback URL requirements](/guides/dashboard/redirects/#allowed-callback-urls). 2. ## Configure passwordless settings [Section titled “Configure passwordless settings”](#configure-passwordless-settings) In the Scalekit dashboard, enable Magic link & OTP and choose your login method. Optional security settings: * **Enforce same-browser origin**: Users must complete magic-link auth in the same browser they started in. * **Issue new credentials on resend**: Each resend generates a fresh code or link and invalidates the previous one. ![](/.netlify/images?url=_astro%2F1.C37ffu3h.png\&w=2221\&h=1207\&dpl=69cce21a4f77360008b1503a) 3. ## Redirect users to sign up (or) login [Section titled “Redirect users to sign up (or) login”](#redirect-users-to-sign-up-or-login) Create an authorization URL and redirect users to Scalekit’s sign-in page. Include: | Parameter | Description | | -------------- | --------------------------------------------------------------------------------- | | `redirect_uri` | Your app’s callback endpoint (for example, `https://your-app.com/auth/callback`). | | `client_id` | Your Scalekit application identifier (scoped to the environment). | | `login_hint` | The user’s email address to receive the verification email. | **Example implementation** * Node.js ```javascript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 // Initialize the SDK client 3 const scalekit = new ScalekitClient( 4 '', 5 '', 6 '', 7 ); 8 9 const options = {}; 10 11 options['loginHint'] = 'user@example.com'; 12 13 const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options); 14 // Generated URL will look like: 15 // https:///oauth/authorize?response_type=code&client_id=skc_1234&scope=openid%20profile%20email&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback 16 17 res.redirect(authorizationUrl); ``` * Python ```python 1 from scalekit import ScalekitClient, AuthorizationUrlOptions, CodeAuthenticationOptions 2 3 # Initialize the SDK client 4 scalekit = ScalekitClient( 5 '', 6 '', 7 '' 8 ) 9 10 options = AuthorizationUrlOptions() 11 12 # Authorization URL with login hint 13 options.login_hint = 'user@example.com' 14 15 authorization_url = scalekit.get_authorization_url(redirect_uri, options) 16 # Generated URL will look like: 17 # https:///oauth/authorize?response_type=code&client_id=skc_1234&scope=openid%20profile%20email&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback 18 19 return redirect(authorization_url) ``` * Go ```go 1 import ( 2 "github.com/scalekit-inc/scalekit-sdk-go" 3 ) 4 5 func main() { 6 // Initialize the SDK client 7 scalekitClient := scalekit.NewScalekitClient( 8 "", 9 "", 10 "" 11 ) 12 13 options := scalekitClient.AuthorizationUrlOptions{} 14 // User's email domain detects the correct enterprise SSO connection. 15 options.LoginHint = "user@example.com" 16 17 authorizationURL := scalekitClient.GetAuthorizationUrl( 18 redirectUrl, 19 options, 20 ) 21 // Next step is to redirect the user to this authorization URL 22 } 23 24 // Redirect the user to this authorization URL ``` * 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 // Initialize the SDK client 10 ScalekitClient scalekitClient = new ScalekitClient( 11 "", 12 "", 13 "" 14 ); 15 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 16 // User's email domain detects the correct enterprise SSO connection. 17 options.setLoginHint("user@example.com"); 18 try { 19 String url = scalekitClient 20 .authentication() 21 .getAuthorizationUrl(redirectUrl, options) 22 .toString(); 23 } catch (Exception e) { 24 System.out.println(e.getMessage()); 25 } 26 } 27 } 28 // Redirect the user to this authorization URL ``` This redirects users to Scalekit’s authentication flow. After verification, they return to your application. Example authorization URL Example authorization URL ```sh 1 /oauth/authorize? 2 client_id=skc_122056050118122349527& 3 redirect_uri=https://yourapp.com/auth/callback& 4 login_hint=user@example.com& 5 response_type=code& 6 scope=openid%20profile%20email& 7 state=jAy-state1-gM4fdZdV22nqm6Q_jAy-XwpYdYFh..2nqm6Q ``` At your `redirect_uri`, handle the callback to exchange the code for the user profile. Ensure this URL is registered as an Allowed Callback URI in the dashboard. Headless passwordless authentication You can implement passwordless authentication without relying on Scalekit’s hosted login pages. This approach lets you build your own UI for collecting verification codes or handling magic links, giving you complete control over the user experience. [Learn about headless passwordless implementation](/passwordless/quickstart) 4. ## Get user details from the callback [Section titled “Get user details from the callback”](#get-user-details-from-the-callback) Scalekit redirects to your `redirect_uri` with an authorization code. Exchange it server-side for the user’s profile. Validation attempt limits To protect your application, Scalekit limits a user to **five** attempts to enter the correct OTP within a ten-minute window for each authentication request. If the user exceeds this limit, they must restart the authentication process. Always perform the code exchange on the server to validate the code and return the authenticated user’s profile. * Node.js Fetch user profile ```javascript 1 // Handle oauth redirect_url, fetch code and error_description from request params 2 const { code, error, error_description } = req.query; 3 4 if (error) { 5 // Handle errors 6 } 7 8 const result = await scalekit.authenticateWithCode(code, redirectUri); 9 const userEmail = result.user.email; 10 11 // Next step: create a session for this user and allow access ``` * Python Fetch user profile ```py 1 # Handle oauth redirect_url, fetch code and error_description from request params 2 code = request.args.get('code') 3 error = request.args.get('error') 4 error_description = request.args.get('error_description') 5 6 if error: 7 raise Exception(error_description) 8 9 result = scalekit.authenticate_with_code(code, '') 10 11 # result.user has the authenticated user's details 12 user_email = result.user.email 13 14 # Next step: create a session for this user and allow access ``` * Go Fetch user profile ```go 1 // Handle oauth redirect_url, fetch code and error_description from request params 2 code := r.URL.Query().Get("code") 3 errorCode := r.URL.Query().Get("error") 4 errorDescription := r.URL.Query().Get("error_description") 5 6 if errorCode != "" { 7 // Handle errors - include errorDescription for context 8 return fmt.Errorf("OAuth error: %s - %s", errorCode, errorDescription) 9 } 10 11 result, err := scalekitClient.AuthenticateWithCode(r.Context(), code, redirectUrl) 12 13 if err != nil { 14 // Handle errors 15 } 16 17 // result.User has the authenticated user's details 18 userEmail := result.User.Email 19 20 // Next step: create a session for this user and allow access ``` * Java Fetch user profile ```java 1 // Handle oauth redirect_url, fetch code and error_description from request params 2 String code = request.getParameter("code"); 3 String error = request.getParameter("error"); 4 String errorDescription = request.getParameter("error_description"); 5 6 if (error != null && !error.isEmpty()) { 7 // Handle errors 8 return; 9 } 10 11 try { 12 AuthenticationResponse result = scalekit.authentication().authenticateWithCode(code, redirectUrl); 13 String userEmail = result.getIdTokenClaims().getEmail(); 14 15 // Next step: create a session for this user and allow access 16 } catch (Exception e) { 17 // Handle errors 18 } ``` The `result` object * Result object ```js { user: { email: "john.doe@example.com" // Authenticated user's email address }, idToken: "", // ID token (JWT) containing user profile claims accessToken: "", // Access token (JWT) for calling backend APIs on behalf of the user expiresIn: 899 // Time in seconds } ``` * Decoded ID token ```json { "alg": "RS256", "kid": "snk_82937465019283746", "typ": "JWT" }.{ "amr": [ "conn_92847563920187364" ], "at_hash": "j8kqPm3nRt5Kx2Vy9wL_Zp", "aud": [ "skc_73645291837465928" ], "azp": "skc_73645291837465928", "c_hash": "Hy4k2M9pWnX7vqR8_Jt3bg", "client_id": "skc_73645291837465928", "email": "alice.smith@example.com", "email_verified": true, "exp": 1751697469, "iat": 1751438269, "iss": "https://demo-company-dev.scalekit.cloud", "sid": "ses_83746592018273645", "sub": "conn_92847563920187364;alice.smith@example.com" // A scalekit user ID is sent if user management is enabled }.[Signature] ``` * Decoded access token ```json { "alg": "RS256", "kid": "snk_794467716206433", "typ": "JWT" }.{ "iss": "https://acme-corp-dev.scalekit.cloud", "sub": "conn_794467724427269;robert.wilson@acme.com", "aud": [ "skc_794467724259497" ], "exp": 1751439169, "iat": 1751438269, "nbf": 1751438269, "client_id": "skc_794467724259497", "jti": "tkn_794754665320942", // External identifiers if updated on Scalekit "xoid": "ext_org_123", // Organization ID "xuid": "ext_usr_456" // User ID }.[Signature] ``` Congratulations! Your application now supports passwordless authentication. Users can sign in securely by: * Entering a verification code sent to their email * Clicking a magic link sent to their email To complete the implementation, [create a session](/authenticate/fsa/manage-session/) for the user to allow access to protected resources. --- # 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 --- # Headless email API for magic link and OTP > Implement email OTP or magic link using direct API calls with full control over UX This guide shows you how to implement magic link and OTP authentication using Scalekit’s headless APIs. You send either a one-time passcode (OTP) or a magic link to the user’s email and then verify their identity. Magic link and OTP offer two email-based authentication methods—clickable links or one-time passcodes—so users can sign in without passwords. You control the UI and user flows, while Scalekit provides the backend authentication infrastructure. See the integration in action [Play](https://youtube.com/watch?v=8e4ZH-Aemg4) Review the authentication sequence Coming soon ### Build with a coding agent * Claude Code ```bash /plugin marketplace add scalekit-inc/claude-code-authstack ``` ```bash /plugin install full-stack-auth@scalekit-auth-stack ``` * GitHub Copilot CLI ```bash copilot plugin marketplace add scalekit-inc/github-copilot-authstack ``` ```bash copilot plugin install full-stack-auth@scalekit-auth-stack ``` * 40+ agents ```bash npx skills add scalekit-inc/skills --skill implementing-scalekit-fsa ``` [Continue building with AI →](/dev-kit/build-with-ai/full-stack-auth/) *** 1. ## Set up Scalekit [Section titled “Set up Scalekit”](#set-up-scalekit) Install the Scalekit SDK to your project. * 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` Your application is responsible for verifying users and initiating sessions, while Scalekit securely manages authentication tokens to ensure the verification process is completed successfully 2. ## Configure magic link and OTP settings [Section titled “Configure magic link and OTP settings”](#configure-magic-link-and-otp-settings) In the Scalekit dashboard, enable magic link and OTP and choose your login method. Optional security settings: * **Enforce same-browser origin**: Users must complete magic-link auth in the same browser they started in. * **Issue new credentials on resend**: Each resend generates a fresh code or link and invalidates the previous one. ![](/.netlify/images?url=_astro%2F1.C37ffu3h.png\&w=2221\&h=1207\&dpl=69cce21a4f77360008b1503a) 3. ## Send verification email [Section titled “Send verification email”](#send-verification-email) The first step in the magic link and OTP flow is to send a verification email to the user’s email address. This email contains either a **one-time passcode (OTP), a magic link, or both** based on your selection in the Scalekit dashboard. Follow these steps to implement the verification email flow: 1. Create a form to collect the user’s email address 2. Call the passwordless API (magic link and OTP) when the form is submitted 3. Handle the response to provide feedback to the user API endpoint ```http POST /api/v1/passwordless/email/send ``` **Example implementation** * cURL Send a verification code to user's email ```sh 1 curl -L '/api/v1/passwordless/email/send' \ 2 -H 'Content-Type: application/json' \ 3 -H 'Authorization: Bearer eyJh..' \ 4 --data-raw '{ 5 "email": "john.doe@example.com", 6 "expires_in": 300, 7 "state": "jAy-state1-gM4fdZ...2nqm6Q", 8 "template": "SIGNIN", 9 10 "magiclink_auth_uri": "https://yourapp.com/passwordless/verify", 11 "template_variables": { 12 "custom_variable_key": "custom_variable_value" 13 } 14 }' 15 16 # Response 6 collapsed lines 17 # { 18 # "auth_request_id": "jAy-state1-gM4fdZ...2nqm6Q" 19 # "expires_at": "1748696575" 20 # "expires_in": 100 21 # "passwordless_type": "OTP" | "LINK" | "LINK_OTP" 22 # } ``` Request parameters | Parameter | Required | Description | | -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `email` | Yes | Recipient’s email address string | | `expires_in` | No | Code expiration time in seconds (default: 300) number | | `state` | No | OIDC state parameter for request validation string | | `template` | No | Email template to use (`SIGNIN` or `SIGNUP`) string | | `magiclink_auth_uri` | No | Magic Link URI that will be sent to your user to complete the authentication flow. If the URL is of the format `https://yourapp.com/passwordless/verify`, the magic link sent to your user via email will be `https://yourapp.com/passwordless/verify?link_token=`. Required if you selected Link or Link + OTP as your authentication method.string | | `template_variables` | No | Pass variables to be used in the email template sent to the user. You may include up to 30 key-value pairs to reference in the email template. object | Response parameters | Parameters | Description | | ------------------- | ----------------------------------------------------------------------------------------------------- | | `auth_request_id` | A unique identifier for the authentication request that can be used to verify the code string | | `expires_at` | Unix timestamp indicating when the verification code will expire string | | `expires_in` | The time in seconds after which the verification code will expire. Default is 100 seconds number | | `passwordless_type` | The type of magic link and OTP authentication. Currently supports `OTP`, `LINK` and `LINK_OTP` string | * Node.js ```js 1 const options = { 2 template: "SIGNIN", 3 state: "jAy-state1-...2nqm6Q", 4 expiresIn: 300, 5 // Required if you selected Link or Link+OTP as your authentication method 6 magiclinkAuthUri: "https://yourapp.com/passwordless/verify", 7 templateVariables: { 8 employeeID: "EMP523", 9 teamName: "Alpha Team", 10 }, 11 }; 12 13 const sendResponse = await scalekit.passwordless 14 .sendPasswordlessEmail( 15 "", 16 options 17 ); 18 19 // sendResponse = { 20 // authRequestId: string, 21 // expiresAt: number, // seconds since epoch 22 // expiresIn: number, // seconds 23 // passwordlessType: string // "OTP" | "LINK" | "LINK_OTP" 24 // } ``` Request parameters | Parameter | Required | Description | | -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `email` | Yes | The email address to send the magic link or OTP verification code to string | | `template` | No | The template type (`SIGNIN`/`SIGNUP`) string | | `state` | No | Optional state parameter to maintain state between request and callback string | | `expiresIn` | No | Optional expiration time in seconds (default: 300) number | | `magiclinkAuthUri` | No | Magic Link URI that will be sent to your user to complete the authentication flow. If the URL is of the format `https://yourapp.com/passwordless/verify`, the magic link sent to your user via email will be `https://yourapp.com/passwordless/verify?link_token=`. Required if you selected Link or Link + OTP as your authentication method.string | | `template_variables` | No | Pass variables to be used in the email template sent to the user. You may include up to 30 key-value pairs to reference in the email template. object | Response parameters | Parameters | Description | | ------------------ | ------------------------------------------------------------------------------ | | `authRequestId` | Unique identifier for the magic link and OTP authentication request string | | `expiresAt` | Expiration time in seconds since epoch number | | `expiresIn` | Expiration time in seconds number | | `passwordlessType` | Type of magic link and OTP authentication (`OTP`, `LINK` or `LINK_OTP`) string | * Python ```python 1 response = client.passwordless.send_passwordless_email( 2 email="john.doe@example.com", 3 template="SIGNIN", # or "SIGNUP", "UNSPECIFIED" 4 expires_in=300, 5 magiclink_auth_uri="https://yourapp.com/passwordless/verify", 6 template_variables={ 7 "employeeID": "EMP523", 8 "teamName": "Alpha Team", 9 }, 10 ) 11 12 # Extract auth request ID from response 13 auth_request_id = response[0].auth_request_id ``` * Go ```go 1 // Send a passwordless email (assumes you have an initialized `client` and `ctx`) 2 templateType := scalekit.TemplateTypeSignin 3 resp, err := scalekitClient.Passwordless().SendPasswordlessEmail( 4 ctx, 5 "john.doe@example.com", 6 &scalekit.SendPasswordlessOptions{ 7 Template: &templateType, 8 State: "jAy-state1-gM4fdZ...2nqm6Q", 9 ExpiresIn: 300, 10 MagiclinkAuthUri: "https://yourapp.com/passwordless/verify", // required if Link or Link+OTP 11 TemplateVariables: map[string]string{ 12 "employeeID": "EMP523", 13 "teamName": "Alpha Team", 14 }, 15 }, 16 ) 17 18 // resp contains: AuthRequestId, ExpiresAt, ExpiresIn, PasswordlessType ``` Request parameters | Parameter | Required | Description | | ------------------- | -------- | --------------------------------------------------------------------------- | | `email` | Yes | The email address to send the magic link or OTP verification code to string | | `MagiclinkAuthUri` | No | Magic Link URI for authentication string | | `State` | No | Optional state parameter string | | `Template` | No | Email template type (`SIGNIN`/`SIGNUP`) string | | `ExpiresIn` | No | Expiration time in seconds number | | `TemplateVariables` | No | Key-value pairs for email template object | Response parameters | Parameters | Description | | ------------------ | ------------------------------------------------------------------------------ | | `AuthRequestId` | Unique identifier for the magic link and OTP authentication request string | | `ExpiresAt` | Expiration time in seconds since epoch number | | `ExpiresIn` | Expiration time in seconds number | | `PasswordlessType` | Type of magic link and OTP authentication (`OTP`, `LINK` or `LINK_OTP`) string | * Java ```java 1 import java.util.HashMap; 2 import java.util.Map; 3 4 TemplateType templateType = TemplateType.SIGNIN; 5 Map templateVariables = new HashMap<>(); 6 templateVariables.put("employeeID", "EMP523"); 7 templateVariables.put("teamName", "Alpha Team"); 8 9 SendPasswordlessOptions options = new SendPasswordlessOptions(); 10 options.setTemplate(templateType); 11 options.setExpiresIn(300); 12 options.setMagiclinkAuthUri("https://yourapp.com/passwordless/verify"); 13 options.setTemplateVariables(templateVariables); 14 15 SendPasswordlessResponse response = passwordlessClient.sendPasswordlessEmail( 16 "john.doe@example.com", 17 options 18 ); 19 20 String authRequestId = response.getAuthRequestId(); ``` 4. ### Resend a verification email [Section titled “Resend a verification email”](#resend-a-verification-email) Users can request a new verification email if they need one. Use the following endpoint to resend an OTP or magic link email. * cURL Request ```diff 1 curl -L '/api/v1/passwordless/email/resend' \ 2 -H 'Content-Type: application/json' \ 3 -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsIm..' \ 4 -d '{ 5 "auth_request_id": "jAy-state1-gM4fdZ...2nqm6Q" 6 }' 7 8 # Response 9 10 # { 11 12 # "auth_request_id": "jAy-state1-gM4fdZ...2nqm6Q" 13 14 # "expires_at": "1748696575" 15 16 # "expires_in": 300 17 18 # "passwordless_type": "OTP" | "LINK" | "LINK_OTP" 19 20 # } ``` Request parameters | Parameters | Required | Description | | ----------------- | -------- | --------------------------------------------------------------------------------- | | `auth_request_id` | Yes | The unique identifier for the authentication request that was sent earlier string | Response parameters | Parameters | Description | | ------------------- | ----------------------------------------------------------------------------------------------------- | | `auth_request_id` | A unique identifier for the authentication request that can be used to verify the code string | | `expires_at` | Unix timestamp indicating when the verification code will expire string | | `expires_in` | The time in seconds after which the verification code will expire. Default is 300 seconds number | | `passwordless_type` | The type of magic link and OTP authentication. Currently supports `OTP`, `LINK` and `LINK_OTP` string | * Node.js ```js 1 const { authRequestId } = sendResponse; 2 const resendResponse = await scalekit.passwordless 3 .resendPasswordlessEmail( 4 authRequestId 5 ); 6 7 // resendResponse = { 8 // authRequestId: "jAy-state1-gM4fdZ...2nqm6Q", 9 // expiresAt: "1748696575", 10 // expiresIn: "300", 11 // passwordlessType: "OTP" | "LINK" | "LINK_OTP" 12 // } ``` Request parameters | Parameters | Required | Description | | --------------- | -------- | --------------------------------------------------------------------------------- | | `authRequestId` | Yes | The unique identifier for the authentication request that was sent earlier string | Response parameters | Parameters | Description | | ------------------ | -------------------------------------------------------------------------- | | `authRequestId` | Unique identifier for the magic link and OTP authentication request string | | `expiresAt` | Expiration time in seconds since epoch number | | `expiresIn` | Expiration time in seconds. Default is 300 seconds number | | `passwordlessType` | `OTP`, `LINK` or `LINK_OTP` string | * Python ```python 1 resend_response = client.passwordless.resend_passwordless_email( 2 auth_request_id=auth_request_id, 3 ) 4 5 new_auth_request_id = resend_response[0].auth_request_id ``` * Go ```go 1 // Resend passwordless email for an existing auth request 2 resendResp, err := scalekitClient.Passwordless().ResendPasswordlessEmail( 3 ctx, // context.Context (e.g., context.Background()) 4 authRequestId, // string: from the send email response 5 ) 6 7 if err != nil { 8 // handle error (log, return HTTP 400/500, etc.) 9 // ... 10 } 11 12 // resendResp is a pointer to ResendPasswordlessResponse struct: 13 // type ResendPasswordlessResponse struct { 14 // AuthRequestId string // Unique ID for the passwordless request 15 // ExpiresAt int64 // Unix timestamp (seconds since epoch) 16 // ExpiresIn int // Expiry duration in seconds 17 // PasswordlessType string // "OTP", "LINK", or "LINK_OTP" 18 // } ``` Request parameters | Parameters | Required | Description | | --------------- | -------- | --------------------------------------------------------------------------------- | | `authRequestId` | Yes | The unique identifier for the authentication request that was sent earlier string | Response parameters | Parameters | Description | | ------------------ | -------------------------------------------------------------------------- | | `AuthRequestId` | Unique identifier for the magic link and OTP authentication request string | | `ExpiresAt` | Expiration time in seconds since epoch number | | `ExpiresIn` | Expiration time in seconds. Default is 300 seconds number | | `PasswordlessType` | `OTP`, `LINK` or `LINK_OTP` string | * Java ```java SendPasswordlessResponse resendResponse = passwordlessClient.resendPasswordlessEmail(authRequestId); ``` If you enabled **Enable new Magic link & OTP credentials on resend** in the Scalekit dashboard, a new verification code or magic link will be sent each time the user requests a new one. Rate limits Scalekit enforces a rate limit of 2 magic link and OTP emails per minute per email address. This limit includes both initial sends and resends. 5. ### Verify the user’s identity [Section titled “Verify the user’s identity”](#verify-the-users-identity) Once the user receives the verification email, * If it is a verification code, they’ll enter it in your application. Use the following endpoint to validate the code and complete authentication. * If it is a magic link, they’ll click the link in the email to verify their address. Capture the `link_token` query parameter and use it to verify. * For additional security with magic links, if you enabled “Enforce same browser origin” in the dashboard, include the `auth_request_id` in the verification request. - Verification code 1. Create a form to collect the verification code 2. Call the verification API when the form is submitted to verify the code 3. Handle the response to either grant access or show an error API endpoint ```http POST /api/v1/passwordless/email/verify ``` **Example implementation** * cURL Request ```diff curl -L '/api/v1/passwordless/email/verify' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsIm..' \ -d '{ "code": "123456", "auth_request_id": "YC4QR-dVZVtNNVHcHwrnHNDV..." }' ``` Request parameters | Parameters | Required | Description | | ----------------- | -------- | ---------------------------------------------------------------------------- | | `code` | Yes | The verification code entered by the user string | | `auth_request_id` | Yes | The request ID from the response when the verification email was sent string | Response parameters | Parameters | Description | | ------------------- | ----------------------------------------------------------------------------------------------------- | | `email` | The email address of the user string | | `state` | The state parameter that was passed in the original request string | | `template` | The template that was used for the verification code string | | `passwordless_type` | The type of magic link and OTP authentication. Currently supports `OTP`, `LINK` and `LINK_OTP` string | * Node.js ```js 1 const { authRequestId } = sendResponse; 2 const verifyResponse = await scalekit.passwordless 3 .verifyPasswordlessEmail( 4 { code: "123456"}, 5 authRequestId 6 ); 7 8 // verifyResponse = { 9 // "email": "saifshine7@gmail.com", 10 // "state": "jAy-state1-gM4fdZdV22nqm6Q_j..", 11 // "template": "SIGNIN", 12 // "passwordless_type": "OTP" | "LINK" | "LINK_OTP" 13 // } ``` Request parameters | Parameters | Required | Description | | --------------- | -------- | --------------------------------------------------------------------------------- | | `options.code` | Yes | The verification code received by the user string | | `authRequestId` | Yes | The unique identifier for the authentication request that was sent earlier string | Response parameters | Parameters | Description | | ------------------ | ----------------------------------------------------------------------------------------------------- | | `email` | The email address of the user string | | `state` | The state parameter that was passed in the original request string | | `template` | The template that was used for the verification code string | | `passwordlessType` | The type of magic link and OTP authentication. Currently supports `OTP`, `LINK` and `LINK_OTP` string | * Python ```python 1 verify_response = client.passwordless.verify_passwordless_email( 2 code="123456", # OTP code received via email 3 auth_request_id=auth_request_id, 4 ) 5 6 # User verified successfully 7 user_email = verify_response[0].email ``` * Go ```go 1 // Verify with OTP code 2 verifyResponse, err := scalekitClient.Passwordless().VerifyPasswordlessEmail( 3 ctx, 4 &scalekit.VerifyPasswordlessOptions{ 5 Code: "123456", // OTP code 6 AuthRequestId: authRequestId, 7 }, 8 ) 9 10 if err != nil { 11 // Handle error 12 return 13 } 14 15 // verifyResp contains the verified user's info 16 // type VerifyPasswordLessResponse struct { 17 // Email string 18 // State string 19 // Template string // SIGNIN | SIGNUP 20 // PasswordlessType string // OTP | LINK | LINK_OTP 21 // } ``` Request parameters | Parameters | Required | Description | | ----------------------- | -------- | --------------------------------------------------------------------------------- | | `options.Code` | Yes | The verification code received by the user string | | `options.AuthRequestId` | Yes | The unique identifier for the authentication request that was sent earlier string | Response parameters | Parameters | Description | | ------------------ | ------------------------------------------------------------------ | | `Email` | The email address of the user string | | `State` | The state parameter that was passed in the original request string | | `Template` | The template that was used (`SIGNIN` or `SIGNUP`) string | | `PasswordlessType` | `OTP`, `LINK` or `LINK_OTP` string | * Java ```java 1 // Verify with OTP code 2 VerifyPasswordlessOptions verifyOptions = new VerifyPasswordlessOptions(); 3 verifyOptions.setCode("123456"); // OTP code 4 verifyOptions.setAuthRequestId(authRequestId); 5 6 VerifyPasswordLessResponse verifyResponse = passwordlessClient.verifyPasswordlessEmail(verifyOptions); 7 8 // User verified successfully 9 String userEmail = verifyResponse.getEmail(); ``` - Magic link verification Request ```diff curl -L '/api/v1/passwordless/email/verify' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsIm..' \ -d '{ "code": "123456", "auth_request_id": "YC4QR-dVZVtNNVHcHwrnHNDV..." }' ``` Request parameters | Parameters | Required | Description | | ----------------- | -------- | ---------------------------------------------------------------------------- | | `code` | Yes | The verification code entered by the user string | | `auth_request_id` | Yes | The request ID from the response when the verification email was sent string | Response parameters | Parameters | Description | | ------------------- | ----------------------------------------------------------------------------------------------------- | | `email` | The email address of the user string | | `state` | The state parameter that was passed in the original request string | | `template` | The template that was used for the verification code string | | `passwordless_type` | The type of magic link and OTP authentication. Currently supports `OTP`, `LINK` and `LINK_OTP` string | - cURL ```js 1 const { authRequestId } = sendResponse; 2 const verifyResponse = await scalekit.passwordless 3 .verifyPasswordlessEmail( 4 { code: "123456"}, 5 authRequestId 6 ); 7 8 // verifyResponse = { 9 // "email": "saifshine7@gmail.com", 10 // "state": "jAy-state1-gM4fdZdV22nqm6Q_j..", 11 // "template": "SIGNIN", 12 // "passwordless_type": "OTP" | "LINK" | "LINK_OTP" 13 // } ``` Request parameters | Parameters | Required | Description | | --------------- | -------- | --------------------------------------------------------------------------------- | | `options.code` | Yes | The verification code received by the user string | | `authRequestId` | Yes | The unique identifier for the authentication request that was sent earlier string | Response parameters | Parameters | Description | | ------------------ | ----------------------------------------------------------------------------------------------------- | | `email` | The email address of the user string | | `state` | The state parameter that was passed in the original request string | | `template` | The template that was used for the verification code string | | `passwordlessType` | The type of magic link and OTP authentication. Currently supports `OTP`, `LINK` and `LINK_OTP` string | - Node.js ```python 1 verify_response = client.passwordless.verify_passwordless_email( 2 code="123456", # OTP code received via email 3 auth_request_id=auth_request_id, 4 ) 5 6 # User verified successfully 7 user_email = verify_response[0].email ``` - Python ```go 1 // Verify with OTP code 2 verifyResponse, err := scalekitClient.Passwordless().VerifyPasswordlessEmail( 3 ctx, 4 &scalekit.VerifyPasswordlessOptions{ 5 Code: "123456", // OTP code 6 AuthRequestId: authRequestId, 7 }, 8 ) 9 10 if err != nil { 11 // Handle error 12 return 13 } 14 15 // verifyResp contains the verified user's info 16 // type VerifyPasswordLessResponse struct { 17 // Email string 18 // State string 19 // Template string // SIGNIN | SIGNUP 20 // PasswordlessType string // OTP | LINK | LINK_OTP 21 // } ``` Request parameters | Parameters | Required | Description | | ----------------------- | -------- | --------------------------------------------------------------------------------- | | `options.Code` | Yes | The verification code received by the user string | | `options.AuthRequestId` | Yes | The unique identifier for the authentication request that was sent earlier string | Response parameters | Parameters | Description | | ------------------ | ------------------------------------------------------------------ | | `Email` | The email address of the user string | | `State` | The state parameter that was passed in the original request string | | `Template` | The template that was used (`SIGNIN` or `SIGNUP`) string | | `PasswordlessType` | `OTP`, `LINK` or `LINK_OTP` string | - Go ```java 1 // Verify with OTP code 2 VerifyPasswordlessOptions verifyOptions = new VerifyPasswordlessOptions(); 3 verifyOptions.setCode("123456"); // OTP code 4 verifyOptions.setAuthRequestId(authRequestId); 5 6 VerifyPasswordLessResponse verifyResponse = passwordlessClient.verifyPasswordlessEmail(verifyOptions); 7 8 // User verified successfully 9 String userEmail = verifyResponse.getEmail(); ``` - Java To support magic link verification, add a callback endpoint in your application typically at `https://your-app.com/passwordless/verify`. Implement it to verify the magic link token and complete the user authentication process. 1. Create a verification endpoint in your application to handle the magic link verification. This is the endpoint that the user lands in when they click the link in the email. 2. Capture the magic link token from the `link_token` request parameter from the URL. 3. Call the verification API when the user clicks the link in the email. 4. Based on token verification, complete the authentication process or show an error with an appropriate error message. API endpoint ```http POST /api/v1/passwordless/email/verify ``` **Example implementation** * cURL Request ```diff curl -L '/api/v1/passwordless/email/verify' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsIm..' \ -d '{ "link_token": "a4143d8f-...c846ed91e_l", "auth_request_id": "YC4QR-dVZVtNNVHcHwrnHNDV..." // (optional) }' ``` Request parameters | Parameters | Required | Description | | ----------------- | -------- | ------------------------------------------------------------------------ | | `link_token` | Yes | The link token received by the user string | | `auth_request_id` | No | The request ID you received when the verification email was sent. string | Auth request ID If you use Magic Link or Magic Link & OTP and have enabled same browser origin enforcement in the Scalekit dashboard, it is required to include the auth request ID in your request. Response parameters | Parameters | Description | | ------------------- | ----------------------------------------------------------------------------------------------------- | | `email` | The email address of the user string | | `state` | The state parameter that was passed in the original request string | | `template` | The template that was used for the verification code string | | `passwordless_type` | The type of magic link and OTP authentication. Currently supports `OTP`, `LINK` and `LINK_OTP` string | * Node.js ```js 1 // User clicks the magic link in their email 2 // Example magic link: https://yourapp.com/passwordless/verify?link_token=a4143d8f-d13d-415c-8f5a-5a5c846ed91e_l 3 4 // 2. Express endpoint to handle the magic link verification 5 app.get('/passwordless/verify', async (req, res) => { 6 const { link_token } = req.query; 7 8 try { 9 // 3. Verify the magic link token with Scalekit 10 const verifyResponse = await scalekit.passwordless 11 .verifyPasswordlessEmail( 12 { linkToken: link_token }, 13 authRequestId // (optional) sendResponse.authRequestId 14 ); 7 collapsed lines 15 16 // 4. Successfully log the user in 17 // Set session/token and redirect to dashboard 18 res.redirect('/dashboard'); 19 } catch (error) { 20 res.status(400).json({ 21 error: 'The magic link is invalid or has expired. Please request a new verification link.' 22 }); 23 } 24 }); 25 26 // verifyResponse = { 27 // "email": "saifshine7@gmail.com", 28 // "state": "jAy-state1-gM4fdZdV22nqm6Q_j..", 29 // "template": "SIGNIN", 30 // "passwordless_type": "OTP" | "LINK" | "LINK_OTP" 31 // } ``` Request parameters | Parameters | Required | Description | | ------------------- | -------- | ---------------------------------------------------------------------------------- | | `options.linkToken` | Yes | The link token received by the user string | | `authRequestId` | No | The unique identifier for the authentication request that was sent earlier. string | Auth request ID If you use Magic Link or Magic Link & OTP and have enabled same browser origin enforcement in the Scalekit dashboard, it is required to include the auth request ID in your request. Response parameters | Parameters | Description | | ------------------ | ----------------------------------------------------------------------------------------------------- | | `email` | The email address of the user string | | `state` | The state parameter that was passed in the original request string | | `template` | The template that was used for the verification code string | | `passwordlessType` | The type of magic link and OTP authentication. Currently supports `OTP`, `LINK` and `LINK_OTP` string | * Python ```python 1 # Verify with magic link token 2 verify_response = client.passwordless.verify_passwordless_email( 3 link_token=link_token, # Magic link token from URL 4 # auth_request_id=auth_request_id, # optional if same-origin enforcement enabled 5 ) 6 7 # User verified successfully 8 user_email = verify_response[0].email ``` * Go ```go 1 verifyResponse, err := scalekitClient.Passwordless().VerifyPasswordlessEmail( 2 ctx, 3 &scalekit.VerifyPasswordlessOptions{ 4 LinkToken: linkToken, // Magic link token 5 }, 6 ) 7 8 if err != nil { 9 // Handle error 10 return 11 } 12 13 // User verified successfully 14 userEmail := verifyResponse.Email ``` * Java ```java 1 // Verify with magic link token 2 VerifyPasswordlessOptions verifyOptions = new VerifyPasswordlessOptions(); 3 verifyOptions.setLinkToken(linkToken); // Magic link token 4 // verifyOptions.setAuthRequestId(authRequestId); // optional if same-origin enforcement enabled 5 6 VerifyPasswordLessResponse verifyResponse = passwordlessClient.verifyPasswordlessEmail(verifyOptions); 7 8 // User verified successfully 9 String userEmail = verifyResponse.getEmail(); ``` - cURL Request ```diff curl -L '/api/v1/passwordless/email/verify' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsIm..' \ -d '{ "link_token": "a4143d8f-...c846ed91e_l", "auth_request_id": "YC4QR-dVZVtNNVHcHwrnHNDV..." // (optional) }' ``` Request parameters | Parameters | Required | Description | | ----------------- | -------- | ------------------------------------------------------------------------ | | `link_token` | Yes | The link token received by the user string | | `auth_request_id` | No | The request ID you received when the verification email was sent. string | Auth request ID If you use Magic Link or Magic Link & OTP and have enabled same browser origin enforcement in the Scalekit dashboard, it is required to include the auth request ID in your request. Response parameters | Parameters | Description | | ------------------- | ----------------------------------------------------------------------------------------------------- | | `email` | The email address of the user string | | `state` | The state parameter that was passed in the original request string | | `template` | The template that was used for the verification code string | | `passwordless_type` | The type of magic link and OTP authentication. Currently supports `OTP`, `LINK` and `LINK_OTP` string | - Node.js ```js 1 // User clicks the magic link in their email 2 // Example magic link: https://yourapp.com/passwordless/verify?link_token=a4143d8f-d13d-415c-8f5a-5a5c846ed91e_l 3 4 // 2. Express endpoint to handle the magic link verification 5 app.get('/passwordless/verify', async (req, res) => { 6 const { link_token } = req.query; 7 8 try { 9 // 3. Verify the magic link token with Scalekit 10 const verifyResponse = await scalekit.passwordless 11 .verifyPasswordlessEmail( 12 { linkToken: link_token }, 13 authRequestId // (optional) sendResponse.authRequestId 14 ); 7 collapsed lines 15 16 // 4. Successfully log the user in 17 // Set session/token and redirect to dashboard 18 res.redirect('/dashboard'); 19 } catch (error) { 20 res.status(400).json({ 21 error: 'The magic link is invalid or has expired. Please request a new verification link.' 22 }); 23 } 24 }); 25 26 // verifyResponse = { 27 // "email": "saifshine7@gmail.com", 28 // "state": "jAy-state1-gM4fdZdV22nqm6Q_j..", 29 // "template": "SIGNIN", 30 // "passwordless_type": "OTP" | "LINK" | "LINK_OTP" 31 // } ``` Request parameters | Parameters | Required | Description | | ------------------- | -------- | ---------------------------------------------------------------------------------- | | `options.linkToken` | Yes | The link token received by the user string | | `authRequestId` | No | The unique identifier for the authentication request that was sent earlier. string | Auth request ID If you use Magic Link or Magic Link & OTP and have enabled same browser origin enforcement in the Scalekit dashboard, it is required to include the auth request ID in your request. Response parameters | Parameters | Description | | ------------------ | ----------------------------------------------------------------------------------------------------- | | `email` | The email address of the user string | | `state` | The state parameter that was passed in the original request string | | `template` | The template that was used for the verification code string | | `passwordlessType` | The type of magic link and OTP authentication. Currently supports `OTP`, `LINK` and `LINK_OTP` string | - Python ```python 1 # Verify with magic link token 2 verify_response = client.passwordless.verify_passwordless_email( 3 link_token=link_token, # Magic link token from URL 4 # auth_request_id=auth_request_id, # optional if same-origin enforcement enabled 5 ) 6 7 # User verified successfully 8 user_email = verify_response[0].email ``` - Go ```go 1 verifyResponse, err := scalekitClient.Passwordless().VerifyPasswordlessEmail( 2 ctx, 3 &scalekit.VerifyPasswordlessOptions{ 4 LinkToken: linkToken, // Magic link token 5 }, 6 ) 7 8 if err != nil { 9 // Handle error 10 return 11 } 12 13 // User verified successfully 14 userEmail := verifyResponse.Email ``` - Java ```java 1 // Verify with magic link token 2 VerifyPasswordlessOptions verifyOptions = new VerifyPasswordlessOptions(); 3 verifyOptions.setLinkToken(linkToken); // Magic link token 4 // verifyOptions.setAuthRequestId(authRequestId); // optional if same-origin enforcement enabled 5 6 VerifyPasswordLessResponse verifyResponse = passwordlessClient.verifyPasswordlessEmail(verifyOptions); 7 8 // User verified successfully 9 String userEmail = verifyResponse.getEmail(); ``` Validation attempt limits To protect your application, Scalekit allows a user only **five** attempts to enter the correct OTP within a ten-minute window. If the user exceeds this limit for an `auth_request_id`, the `/passwordless/email/verify` endpoint returns an **HTTP 429 Too Many Requests** error. To continue, the user must restart the authentication flow. You’ve successfully implemented Magic link & OTP authentication in your application. Users can now sign in securely without passwords by entering a verification code (OTP) or clicking a magic link sent to their email. --- # 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 1 { 2 "event_type": "ORGANIZATION_SSO_ENABLED", 3 "object": "connection", 4 "organization_id": "org_4010340X34236531", // Organization that enabled SSO 5 "message": "Single sign-on connection enabled successfully", 6 "data": { 7 "connection_type": "SSO", 8 "id": "conn_4256075523X312", // Connection ID for API calls 9 "type": "OIDC", // Protocol: OIDC or SAML 10 "provider": "OKTA", // Identity provider configured 11 "enabled": true 12 } 13 } ``` | 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 1 { 2 "event_type": "ORGANIZATION_SSO_DISABLED", 3 "object": "connection", 4 "organization_id": "org_4010340X34236531", // Organization that disabled SSO 5 "message": "Single sign-on connection disabled successfully", 6 "data": { 7 "connection_type": "SSO", 8 "id": "conn_4256075523X312", // Connection ID that was disabled 9 "type": "OIDC", // Protocol: OIDC or SAML 10 "provider": "OKTA", // Identity provider that was configured 11 "enabled": false 12 } 13 } ``` | 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 1 { 2 "event_type": "PORTAL_LOAD_SUCCESS", 3 "object": "session", 4 "message": "The admin portal loaded successfully", 5 "organization_id": "org_43982563588440584", 6 "data": { 7 "expiry": "2025-02-28T12:40:35.911Z" // ISO 8601 timestamp when session expires 8 } 9 } ``` | 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 1 { 2 "event_type": "PORTAL_LOAD_FAILURE", 3 "object": "session", 4 "message": "The admin portal failed to load", 5 "data": { 6 "error_code": "SESSION_EXPIRED" // error code indicating why the session load failed 7 } 8 } ``` | 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 1 { 2 "event_type": "PORTAL_SESSION_WARNING", 3 "object": "session", 4 "message": "The admin portal session will expire in 5 minutes", 5 "organization_id": "org_43982563588440584", 6 "data": { 7 "expiry": "2025-02-28T12:40:35.911Z" // ISO 8601 timestamp when session expires 8 } 9 } ``` | 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 1 { 2 "event_type": "PORTAL_SESSION_EXPIRY", 3 "object": "session", 4 "message": "The admin portal session has expired", 5 "organization_id": "org_43982563588440584", 6 "data": { 7 "expiry": "2025-02-28T12:40:35.911Z" // ISO 8601 timestamp when session expired 8 } 9 } ``` | 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 1 { 2 "event_type": "ORGANIZATION_DIRECTORY_ENABLED", 3 "object": "directory", 4 "organization_id": "org_45716217859670289", // Organization that enabled directory sync 5 "message": "SCIM Provisioning enabled successfully", 6 "data": { 7 "directory_type": "SCIM", // Directory protocol type 8 "id": "dir_45716228982964495", // Directory connection ID for API calls 9 "provider": "MICROSOFT_AD", // Identity provider: OKTA, AZURE_AD, GOOGLE, etc. 10 "enabled": true 11 } 12 } ``` | 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 1 { 2 "event_type": "ORGANIZATION_DIRECTORY_DISABLED", 3 "object": "directory", 4 "organization_id": "org_45716217859670289", // Organization that disabled directory sync 5 "message": "SCIM Provisioning disabled successfully", 6 "data": { 7 "directory_type": "SCIM", // Directory protocol type 8 "id": "dir_45716228982964495", // Directory connection ID that was disabled 9 "provider": "MICROSOFT_AD", // Identity provider that was configured 10 "enabled": false 11 } 12 } ``` | 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 --- # Airtable > Connect to Airtable. Manage databases, tables, records, and collaborate on structured data Connect to Airtable. Manage databases, tables, records, and collaborate on structured data ![Airtable logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/airtable.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Airtable connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Create the Airtable connection in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Actions** → **Connections** and click **+ Create Connection**. Search for **Airtable** and click **Create**. ![Search for Airtable and create a new connection](/.netlify/images?url=_astro%2Fcreate-airtable-connection.CXWGcFJh.png\&w=3024\&h=1616\&dpl=69cce21a4f77360008b1503a) * In the **Configure Airtable Connection** dialog, copy the **Redirect URI**. You will need this when registering your OAuth integration in Airtable. ![Copy the redirect URI from the Configure Airtable Connection dialog](/.netlify/images?url=_astro%2Fconfigure-airtable-connection.B9XkXjqC.png\&w=1538\&h=1614\&dpl=69cce21a4f77360008b1503a) 2. ### Register an OAuth integration in Airtable * Go to the [Airtable Builder Hub](https://airtable.com/create/oauth) and navigate to **OAuth integrations**. Click **Register an OAuth integration**. ![OAuth integrations page in Airtable Builder Hub](/.netlify/images?url=_astro%2Fairtable-oauth-integrations.D5AczkCo.png\&w=3024\&h=1538\&dpl=69cce21a4f77360008b1503a) * Fill in your integration details (name, description, and other required fields). * Under **OAuth redirect URLs**, paste the redirect URI you copied from the Scalekit dashboard. 3. ### Get your client credentials * On your OAuth integration page in the Airtable Builder Hub, find the **Developer details** section. * Copy the **Client ID**. * Click **Generate client secret** and copy the secret value immediately. ![Copy Client ID and generate a client secret from Airtable developer details](/.netlify/images?url=_astro%2Fairtable-developer-details.CtaPm7Zf.png\&w=2468\&h=900\&dpl=69cce21a4f77360008b1503a) 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Actions** → **Connections** and open the Airtable connection you created. * Enter your credentials: * **Client ID** — from the Airtable developer details * **Client Secret** — the generated secret from Airtable * **Scopes** — select the permissions your app needs (for example, `data.records:read`, `data.records:write`, `schema.bases:read`, `schema.bases:write`, `webhook.manage`). See [Airtable OAuth scopes reference](https://airtable.com/developers/web/api/scopes) for the full list. ![Airtable credentials entered in the Scalekit connection configuration](/.netlify/images?url=_astro%2Fairtable-credentials-filled.I9vyzMa4.png\&w=1534\&h=1618\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Airtable account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Airtable in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'airtable'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Airtable:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v0/meta/whoami', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "airtable" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Airtable:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v0/meta/whoami", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Apollo > Connect to Apollo.io to search and enrich B2B contacts and accounts, manage CRM contacts, and automate outreach sequences. Connect to Apollo.io to search and enrich B2B contacts and accounts, manage CRM contacts, and automate outreach sequences. ![Apollo logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/apollo.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Apollo connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Note Apollo restricts contact enrichment (`apollo_enrich_contact`), account search (`apollo_search_accounts`), and contact search (`apollo_search_contacts`) to paid plans. Free plan accounts will get an error when calling these tools. 1. ### Create a connection in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Apollo** and click **Create**. * Click **Use your own credentials** and copy the **Redirect URI**. It looks like: `https:///sso/v1/oauth//callback` | Scope | Required for | | -------------------------- | ------------------------------------ | | `contact_read` | Reading contact details | | `contact_write` | Creating contacts | | `contact_update` | Updating contacts | | `account_read` | Reading account details | | `account_write` | Creating accounts | | `organizations_enrich` | Enriching accounts with Apollo data | | `person_read` | Enriching contacts (paid plans only) | | `emailer_campaigns_search` | Listing email sequences | | `accounts_search` | Searching accounts (paid plans only) | | `contacts_search` | Searching contacts (paid plans only) | Keep this tab open — you’ll return to it in step 3. 2. ### Register an OAuth application in Apollo * Go to [Apollo’s OAuth registration page](https://developer.apollo.io/oauth-registration#/oauth-registration) and sign in with your Apollo account. * Fill in the registration form: * **Application name** — a name to identify your app (e.g., `My Sales Agent`) * **Description** — brief description of what your app does * **Redirect URIs** — paste the redirect URI you copied from Scalekit * Under **Scopes**, select the permissions your agent needs. Use the table below to decide: | Scope | Required for | | -------------------------- | ------------------------------------ | | `contact_read` | Reading contact details | | `contact_write` | Creating contacts | | `contact_update` | Updating contacts | | `account_read` | Reading account details | | `account_write` | Creating accounts | | `organizations_enrich` | Enriching accounts with Apollo data | | `person_read` | Enriching contacts (paid plans only) | | `emailer_campaigns_search` | Listing email sequences | | `accounts_search` | Searching accounts (paid plans only) | | `contacts_search` | Searching contacts (paid plans only) | ![](/.netlify/images?url=_astro%2Fcreate-oauth-app.87W0gk5S.png\&w=1100\&h=720\&dpl=69cce21a4f77360008b1503a) * Click **Register application**. 3. ### Copy your client credentials After registering, Apollo shows the **Client ID** and **Client Secret** for your application. ![](/.netlify/images?url=_astro%2Fclient-credentials.BT_UsNv8.png\&w=1100\&h=500\&dpl=69cce21a4f77360008b1503a) Copy both values now. **The Client Secret is shown only once** — you cannot retrieve it again after navigating away. 4. ### Add credentials in Scalekit * Return to [Scalekit dashboard](https://app.scalekit.com) → **Agent Auth** → **Connections** and open the connection you created in step 1. * Enter the following: * **Client ID** — from Apollo * **Client Secret** — from Apollo * **Permissions** — the same scopes you selected in Apollo ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.DrJGtI2n.png\&w=1496\&h=480\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Apollo account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. * Node.js examples/apollo.ts ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'apollo'; // connection name from Scalekit dashboard 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get credentials from app.scalekit.com → Developers → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 async function main() { 16 try { 17 // Get authorization link and send it to your user 18 const { link } = await actions.getAuthorizationLink({ 19 connectionName, 20 identifier, 21 }); 22 console.log('Authorize Apollo:', link); // present this link to your user for authorization, or click it yourself for testing 23 process.stdout.write('Press Enter after authorizing...'); 24 await new Promise(r => process.stdin.once('data', r)); 25 26 // After the user authorizes, make API calls via Scalekit proxy 27 const result = await actions.request({ 28 connectionName, 29 identifier, 30 path: '/api/v1/contacts/search', 31 method: 'POST', 32 }); 33 console.log(result.data); 34 } catch (err) { 35 console.error('Apollo request failed:', err); 36 process.exit(1); 37 } 38 } 39 40 main().catch((err) => { 41 console.error('Unhandled error:', err); 42 process.exit(1); 43 }); ``` * Python ```python 1 import scalekit.client, os, sys 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "apollo" # connection name from Scalekit dashboard 6 identifier = "user_123" # your unique user identifier 7 8 # Get credentials from app.scalekit.com → Developers → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 15 try: 16 # Get authorization link and send it to your user 17 link_response = scalekit_client.actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 print("Authorize Apollo:", link_response.link) 22 input("Press Enter after authorizing...") 23 24 # After the user authorizes, make API calls via Scalekit proxy 25 result = scalekit_client.actions.request( 26 connection_name=connection_name, 27 identifier=identifier, 28 path="/api/v1/contacts/search", 29 method="POST" 30 ) 31 print(result) 32 except Exception as e: 33 print(f"Apollo request failed: {e}", file=sys.stderr) 34 sys.exit(1) ``` ## Tool list [Section titled “Tool list”](#tool-list) ## `apollo_create_account` [Section titled “apollo\_create\_account”](#apollo_create_account) Create a new account (company) record in your Apollo CRM. Accounts represent organizations and can be linked to contacts. Check for duplicates before creating to avoid double entries. | Name | Type | Required | Description | | -------------- | ------ | -------- | -------------------------------- | | `domain` | string | No | Website domain of the company | | `linkedin_url` | string | No | LinkedIn company page URL | | `name` | string | Yes | Name of the company/account | | `phone_number` | string | No | Main phone number of the company | | `raw_address` | string | No | Physical address of the company | ## `apollo_create_contact` [Section titled “apollo\_create\_contact”](#apollo_create_contact) Create a new contact record in your Apollo CRM. The contact will appear in your Apollo contacts list and can be enrolled in sequences. Check for duplicates before creating to avoid double entries. | Name | Type | Required | Description | | ------------------- | ------ | -------- | ------------------------------------------------ | | `account_id` | string | No | Apollo account ID to associate this contact with | | `email` | string | No | Email address of the contact | | `first_name` | string | Yes | First name of the contact | | `last_name` | string | Yes | Last name of the contact | | `linkedin_url` | string | No | LinkedIn profile URL of the contact | | `organization_name` | string | No | Company name the contact works at | | `phone` | string | No | Phone number of the contact | | `title` | string | No | Job title of the contact | ## `apollo_enrich_account` [Section titled “apollo\_enrich\_account”](#apollo_enrich_account) Enrich a company/account record with Apollo firmographic data using the company’s website domain or name. Returns verified employee count, revenue estimates, industry, tech stack, funding rounds, and social profiles. Consumes Apollo credits per match. | Name | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------------------ | | `domain` | string | No | Website domain of the company to enrich (e.g., acmecorp.com) | | `name` | string | No | Company name to enrich (used if domain is not available) | ## `apollo_enrich_contact` [Section titled “apollo\_enrich\_contact”](#apollo_enrich_contact) Enrich a contact using Apollo’s people matching engine. Provide an email address or name + company to retrieve a verified contact profile. Revealing personal emails or phone numbers consumes additional Apollo credits per successful match. | Name | Type | Required | Description | | ------------------------ | ------- | -------- | -------------------------------------------------------------------------- | | `email` | string | No | Work email address of the contact to enrich | | `first_name` | string | No | First name of the contact to enrich | | `last_name` | string | No | Last name of the contact to enrich | | `linkedin_url` | string | No | LinkedIn profile URL for precise matching | | `organization_name` | string | No | Company name to assist in matching | | `reveal_personal_emails` | boolean | No | Attempt to reveal personal email addresses (consumes extra Apollo credits) | | `reveal_phone_number` | boolean | No | Attempt to reveal direct phone numbers (consumes extra Apollo credits) | ## `apollo_get_account` [Section titled “apollo\_get\_account”](#apollo_get_account) Retrieve the full profile of a company account from Apollo by its ID. Returns detailed firmographic data including employee count, revenue estimates, industry, tech stack, funding information, and social profiles. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------ | | `account_id` | string | Yes | The Apollo account (organization) ID to retrieve | ## `apollo_get_contact` [Section titled “apollo\_get\_contact”](#apollo_get_contact) Retrieve the full profile of a contact from Apollo by their ID. Returns detailed professional information including email, phone, LinkedIn URL, employment history, education, and social profiles. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------- | | `contact_id` | string | Yes | The Apollo contact ID to retrieve | ## `apollo_list_sequences` [Section titled “apollo\_list\_sequences”](#apollo_list_sequences) List available email sequences (Apollo Sequences / Emailer Campaigns) in your Apollo account. Supports filtering by name and pagination. Returns sequence ID, name, status, and step count. | Name | Type | Required | Description | | ---------- | ------- | -------- | ------------------------------------------------ | | `page` | integer | No | Page number for pagination (starts at 1) | | `per_page` | integer | No | Number of sequences to return per page (max 100) | | `search` | string | No | Filter sequences by name (partial match) | ## `apollo_search_accounts` [Section titled “apollo\_search\_accounts”](#apollo_search_accounts) Search Apollo’s company database using firmographic filters such as company name, industry, employee count range, revenue range, and location. Returns matching account records with company details. | Name | Type | Required | Description | | ----------------- | ------- | -------- | --------------------------------------------------------------- | | `company_name` | string | No | Filter accounts by company name (partial match supported) | | `employee_ranges` | string | No | Comma-separated employee count ranges (e.g., 1,10,11,50,51,200) | | `industry` | string | No | Filter accounts by industry vertical | | `keywords` | string | No | Keyword search across company name, description, and domain | | `location` | string | No | Filter accounts by headquarters city, state, or country | | `page` | integer | No | Page number for pagination (starts at 1) | | `per_page` | integer | No | Number of accounts to return per page (max 100) | ## `apollo_search_contacts` [Section titled “apollo\_search\_contacts”](#apollo_search_contacts) Search contacts in your Apollo CRM using filters such as job title, company, and sort order. Returns matching contact records with professional details. Results are paginated. | Name | Type | Required | Description | | -------------- | ------- | -------- | -------------------------------------------------------------------------------- | | `company_name` | string | No | Filter contacts by company name | | `industry` | string | No | Filter contacts by their company’s industry (e.g., Software, Healthcare) | | `keywords` | string | No | Full-text keyword search across contact name, title, company, and bio | | `location` | string | No | Filter contacts by city, state, or country | | `page` | integer | No | Page number for pagination (starts at 1) | | `per_page` | integer | No | Number of contacts to return per page (max 100) | | `seniority` | string | No | Filter by seniority level (e.g., c\_suite, vp, director, manager, senior, entry) | | `title` | string | No | Filter contacts by job title keywords (e.g., VP of Sales) | ## `apollo_update_contact` [Section titled “apollo\_update\_contact”](#apollo_update_contact) Update properties or CRM stage of an existing Apollo contact record by contact ID. Only the provided fields will be updated; omitted fields remain unchanged. | Name | Type | Required | Description | | ------------------- | ------ | -------- | ------------------------------------------ | | `contact_id` | string | Yes | The Apollo contact ID to update | | `contact_stage_id` | string | No | Apollo CRM stage ID to move the contact to | | `email` | string | No | Updated email address for the contact | | `first_name` | string | No | Updated first name | | `last_name` | string | No | Updated last name | | `linkedin_url` | string | No | Updated LinkedIn profile URL | | `organization_name` | string | No | Updated company name | | `phone` | string | No | Updated phone number | | `title` | string | No | Updated job title | --- # DOCUMENT BOUNDARY --- # Asana > Connect to Asana. Manage tasks, projects, teams, and workflow automation Connect to Asana. Manage tasks, projects, teams, and workflow automation ![Asana logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/asana-n.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Asana connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Asana** and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.LLcFm0Aq.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Go to [Asana Developer Console](https://app.asana.com/-/developer_console) and click **Create new app**. Enter an app name. * In the left menu, go to **OAuth**. Under **Redirect URLs**, click **Add redirect URL**, paste the redirect URI from Scalekit, and click **Add**. ![Add redirect URL in Asana Developer Console](/.netlify/images?url=_astro%2Fadd-redirect-uri.CSQko2oO.png\&w=1440\&h=820\&dpl=69cce21a4f77360008b1503a) 2. ### Enable multi-workspace install Optional Enable this if you want users outside your Asana workspace to install the app. * In your app settings, go to **OAuth** → **App permissions**. * Under **App install permissions**, enable **Allow users outside your workspace to install this app**. ![Enable multi-workspace install in Asana](/.netlify/images?url=_astro%2Fenable-distribution.EOc2Xq_i.png\&w=2726\&h=1066\&dpl=69cce21a4f77360008b1503a) 3. ### Get client credentials * In [Asana Developer Console](https://app.asana.com/-/developer_console), select your app. * Under **OAuth**, copy your **Client ID** and **Client Secret**. 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from above) * Client Secret (from above) * Permissions (scopes — see [Asana OAuth scopes reference](https://developers.asana.com/docs/oauth#scopes)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.K4npDEJM.png\&w=1496\&h=480\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Asana account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Asana in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'asana'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Asana:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/api/1.0/users/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "asana" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Asana:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/api/1.0/users/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Attention > Connect to Attention for AI insights, conversations, teams, and workflows Connect to Attention for AI insights, conversations, teams, and workflows ![Attention logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/attention.svg) Supports authentication: API Key ## Usage [Section titled “Usage”](#usage) Connect a user’s Attention account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Attention in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'attention'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Attention:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1/users/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "attention" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Attention:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1/users/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Attio > Connect to Attio CRM to manage contacts, companies, deals, notes, tasks, and lists with a modern relationship management platform. Connect to Attio to manage CRM records, people, companies, deals, tasks, notes, and workspace data ![Attio logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/attio.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Attio OAuth app credentials with Scalekit so it can manage the OAuth 2.0 authentication flow and token lifecycle on your behalf. You’ll need a **Client ID** and **Client Secret** from the [Attio Developer Portal](https://build.attio.com). 1. ### Create a connection in Scalekit and copy the redirect URI * Sign in to your [Scalekit dashboard](https://app.scalekit.com) and go to **Agent Auth** in the left sidebar. * Click **Create Connection**, search for **Attio**, and click **Create**. * On the connection configuration panel, locate the **Redirect URI** field. It looks like: `https:///sso/v1/oauth//callback` * Click the copy icon next to the Redirect URI to copy it to your clipboard. ![Scalekit Agent Auth showing the Redirect URI for the Attio connection](/_astro/use-own-credentials-redirect-uri.YI9B55dT.png) Keep this tab open — you’ll return to it in step 3. 2. ### Register the redirect URI in your Attio OAuth app * Sign in to [build.attio.com](https://build.attio.com) and open the app you want to connect. If you don’t have one yet, click **Create app**. * In the left sidebar, click **OAuth** to open the OAuth settings tab for your app. * You’ll see your **Client ID** and **Client Secret** near the top of the page. Copy both values and save them somewhere safe — you’ll need them in step 3. * Scroll down to the **Redirect URIs** section. Click **+ New redirect URI**. * Paste the Redirect URI you copied from Scalekit into the input field and confirm. ![Attio OAuth app settings showing Client ID, Client Secret, and the Redirect URIs section with the Scalekit callback URL added](/_astro/add-redirect-uri.ChnN8og5.png) Tip Your Attio app must have **“Will people besides you be able to use this app?”** set to **Yes** (or equivalent multi-workspace access) for other users to complete the OAuth flow. Check this under your app’s general settings if authorization fails. 3. ### Add credentials and scopes in Scalekit * Return to your [Scalekit dashboard](https://app.scalekit.com) → **Agent Auth** → **Connections** and open the Attio connection you created in step 1. * Fill in the following fields: * **Client ID** — paste the Client ID from your Attio OAuth app * **Client Secret** — paste the Client Secret from your Attio OAuth app * **Permissions** — select the OAuth scopes your app requires. Choose the minimum scopes needed. Common scopes: | Scope | What it allows | | ------------------------------ | ------------------------------------------- | | `record_permission:read` | Read CRM records (people, companies, deals) | | `record_permission:read-write` | Read and write CRM records | | `object_configuration:read` | Read object and attribute schemas | | `list_configuration:read` | Read list schemas | | `list_entry:read` | Read list entries | | `list_entry:read-write` | Read and write list entries | | `note:read` | Read notes | | `note:read-write` | Read and write notes | | `task:read-write` | Read and write tasks | | `comment:read-write` | Read and write comments | | `webhook:read-write` | Manage webhooks | | `user_management:read` | Read workspace members | For a full list, see the [Attio OAuth scopes reference](https://developers.attio.com/docs/authentication). ![Scalekit connection configuration showing the Client ID, Client Secret, and Permissions fields for the Attio connection](/_astro/add-credentials.BtC76_mk.png) * Click **Save**. Scalekit will validate the credentials and mark the connection as active. ## Usage [Section titled “Usage”](#usage) Connect a user’s Attio workspace and make API calls on their behalf — Scalekit handles OAuth and token management automatically. * Node.js examples/attio.ts ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'attio'; // connection name from Scalekit dashboard 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get credentials from app.scalekit.com → Developers → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 async function main() { 16 try { 17 // Step 1: Send this URL to your user to authorize Attio access 18 const { link } = await actions.getAuthorizationLink({ connectionName, identifier }); 19 console.log('Authorize Attio:', link); // present this link to your user for authorization, or click it yourself for testing 20 process.stdout.write('Press Enter after authorizing...'); 21 await new Promise(r => process.stdin.once('data', r)); 22 23 // Step 2: After the user authorizes, make API calls via Scalekit proxy 24 25 // --- Query people records with a filter --- 26 const people = await actions.request({ 27 connectionName, 28 identifier, 29 path: '/v2/objects/people/records/query', 30 method: 'POST', 31 body: { 32 filter: { 33 email_addresses: [{ email_address: { $eq: 'alice@example.com' } }], 34 }, 35 limit: 10, 36 }, 37 }); 38 console.log('People:', people.data); 39 40 // --- Create a company record --- 41 const company = await actions.request({ 42 connectionName, 43 identifier, 44 path: '/v2/objects/companies/records', 45 method: 'POST', 46 body: { 47 data: { 48 values: { 49 name: [{ value: 'Acme Corp' }], 50 domains: [{ domain: 'acme.com' }], 51 }, 52 }, 53 }, 54 }); 55 const companyId = company.data.data.id.record_id; 56 console.log('Created company:', companyId); 57 58 // --- Create a person record and associate with the company --- 59 const person = await actions.request({ 60 connectionName, 61 identifier, 62 path: '/v2/objects/people/records', 63 method: 'POST', 64 body: { 65 data: { 66 values: { 67 name: [{ first_name: 'Alice', last_name: 'Smith' }], 68 email_addresses: [{ email_address: 'alice@acme.com', attribute_type: 'email' }], 69 company: [{ target_record_id: companyId }], 70 }, 71 }, 72 }, 73 }); 74 const personId = person.data.data.id.record_id; 75 console.log('Created person:', personId); 76 77 // --- Add a note to the person record --- 78 const note = await actions.request({ 79 connectionName, 80 identifier, 81 path: '/v2/notes', 82 method: 'POST', 83 body: { 84 data: { 85 parent_object: 'people', 86 parent_record_id: personId, 87 title: 'Initial outreach', 88 content: 'Spoke with Alice about Q2 pricing. Follow up next week.', 89 format: 'plaintext', 90 }, 91 }, 92 }); 93 console.log('Created note:', note.data.data.id.note_id); 94 95 // --- Create a task linked to the person --- 96 const deadlineAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // 7 days from now 97 const task = await actions.request({ 98 connectionName, 99 identifier, 100 path: '/v2/tasks', 101 method: 'POST', 102 body: { 103 data: { 104 content: 'Send Q2 pricing proposal to Alice', 105 deadline_at: deadlineAt, 106 is_completed: false, 107 linked_records: [{ target_object: 'people', target_record_id: personId }], 108 }, 109 }, 110 }); 111 console.log('Created task:', task.data.data.id.task_id); 112 113 // --- Verify current token and workspace --- 114 const tokenInfo = await actions.request({ 115 connectionName, 116 identifier, 117 path: '/v2/self', 118 method: 'GET', 119 }); 120 console.log('Connected to workspace:', tokenInfo.data.data.workspace.name); 121 console.log('Granted scopes:', tokenInfo.data.data.scopes.join(', ')); 122 } catch (err) { 123 console.error('Attio request failed:', err); 124 process.exit(1); 125 } 126 } 127 128 main(); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "attio" # connection name from Scalekit dashboard 6 identifier = "user_123" # your unique user identifier 7 8 # Get credentials from app.scalekit.com → Developers → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 15 # Step 1: Send this URL to your user to authorize Attio access 16 link_response = scalekit_client.actions.get_authorization_link( 17 connection_name=connection_name, 18 identifier=identifier 19 ) 20 print("Authorize Attio:", link_response.link) 21 22 # Step 2: After the user authorizes, make API calls via Scalekit proxy 23 24 # --- Query people records with a filter --- 25 people = scalekit_client.actions.request( 26 connection_name=connection_name, 27 identifier=identifier, 28 path="/v2/objects/people/records/query", 29 method="POST", 30 json={ 31 "filter": { 32 "email_addresses": [{"email_address": {"$eq": "alice@example.com"}}] 33 }, 34 "limit": 10 35 } 36 ) 37 print("People:", people) 38 39 # --- Create a company record --- 40 company = scalekit_client.actions.request( 41 connection_name=connection_name, 42 identifier=identifier, 43 path="/v2/objects/companies/records", 44 method="POST", 45 json={ 46 "data": { 47 "values": { 48 "name": [{"value": "Acme Corp"}], 49 "domains": [{"domain": "acme.com"}] 50 } 51 } 52 } 53 ) 54 company_id = company["data"]["id"]["record_id"] 55 print("Created company:", company_id) 56 57 # --- Create a person record and associate with the company --- 58 person = scalekit_client.actions.request( 59 connection_name=connection_name, 60 identifier=identifier, 61 path="/v2/objects/people/records", 62 method="POST", 63 json={ 64 "data": { 65 "values": { 66 "name": [{"first_name": "Alice", "last_name": "Smith"}], 67 "email_addresses": [{"email_address": "alice@acme.com", "attribute_type": "email"}], 68 "company": [{"target_record_id": company_id}] 69 } 70 } 71 } 72 ) 73 person_id = person["data"]["id"]["record_id"] 74 print("Created person:", person_id) 75 76 # --- Add a note to the person record --- 77 note = scalekit_client.actions.request( 78 connection_name=connection_name, 79 identifier=identifier, 80 path="/v2/notes", 81 method="POST", 82 json={ 83 "data": { 84 "parent_object": "people", 85 "parent_record_id": person_id, 86 "title": "Initial outreach", 87 "content": "Spoke with Alice about Q2 pricing. Follow up next week.", 88 "format": "plaintext" 89 } 90 } 91 ) 92 print("Created note:", note["data"]["id"]["note_id"]) 93 94 # --- Create a task linked to the person --- 95 task = scalekit_client.actions.request( 96 connection_name=connection_name, 97 identifier=identifier, 98 path="/v2/tasks", 99 method="POST", 100 json={ 101 "data": { 102 "content": "Send Q2 pricing proposal to Alice", 103 "deadline_at": "2026-03-20T17:00:00.000Z", 104 "is_completed": False, 105 "linked_records": [ 106 {"target_object": "people", "target_record_id": person_id} 107 ] 108 } 109 } 110 ) 111 print("Created task:", task["data"]["id"]["task_id"]) 112 113 # --- Verify current token and workspace --- 114 token_info = scalekit_client.actions.request( 115 connection_name=connection_name, 116 identifier=identifier, 117 path="/v2/self", 118 method="GET" 119 ) 120 workspace = token_info["data"]["workspace"]["name"] 121 scopes = token_info["data"]["scopes"] 122 print(f"Connected to workspace: {workspace}") 123 print(f"Granted scopes: {', '.join(scopes)}") ``` Choosing the right filter syntax Attio uses attribute slugs for filtering. Use `attio_list_attributes` to discover available attributes and their slugs before querying. For example, `email_addresses` is a multi-value attribute — filter it as an array condition. ## Tool list [Section titled “Tool list”](#tool-list) ## `attio_create_person` [Section titled “attio\_create\_person”](#attio_create_person) Create a new person record in Attio. Throws an error if a unique attribute like `email_addresses` conflicts with an existing record. The `values` object maps attribute slugs to their typed values — use `attio_list_attributes` on the `people` object first to discover available slugs and types. **Example `values`:** ```json 1 { 2 "name": [{ "first_name": "Alice", "last_name": "Smith" }], 3 "email_addresses": [{ "email_address": "alice@acme.com", "attribute_type": "email" }], 4 "company": [{ "target_record_id": "" }] 5 } ``` | Name | Type | Required | Description | | -------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `values` | object | Yes | Attribute values keyed by attribute slug. Multi-value attributes (e.g., `email_addresses`, `phone_numbers`) must be arrays. Single-value attributes (e.g., `name`) must also be arrays with one item. | ## `attio_list_people` [Section titled “attio\_list\_people”](#attio_list_people) List person records with optional filtering and sorting. Returns paginated results. Use `attio_list_attributes` on `people` to find filterable attribute slugs before building a `filter` object. **Filter example** — find people at a specific company: ```json 1 { "company": [{ "target_record_id": "" }] } ``` **Sort example** — most recently created first: ```json 1 [{ "direction": "desc", "attribute": "created_at" }] ``` | Name | Type | Required | Description | | -------- | ------- | -------- | ----------------------------------------------------------------------------------------- | | `filter` | object | No | Attribute-based filter conditions. Keys are attribute slugs; values are match expressions | | `sorts` | array | No | Array of sort objects, each with `attribute` (slug) and `direction` (`asc` or `desc`) | | `limit` | integer | No | Number of records to return per page (default `500`) | | `offset` | integer | No | Number of records to skip for pagination (default `0`) | ## `attio_get_person` [Section titled “attio\_get\_person”](#attio_get_person) Retrieve a single person record by its `record_id`. Returns all attribute values with temporal and audit metadata, including `created_by_actor` and `active_from`/`active_until` per value. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------- | | `record_id` | string | Yes | UUID of the person record to retrieve | ## `attio_delete_person` [Section titled “attio\_delete\_person”](#attio_delete_person) Permanently delete a person record by its `record_id`. This operation is irreversible and cannot be undone. The underlying contact data, linked tasks, notes, and list entries associated with this person are also affected. | Name | Type | Required | Description | | ----------- | ------ | -------- | ----------------------------------- | | `record_id` | string | Yes | UUID of the person record to delete | ## `attio_create_company` [Section titled “attio\_create\_company”](#attio_create_company) Create a new company record in Attio. Throws an error if a unique attribute like `domains` conflicts with an existing company. Use `attio_list_attributes` on `companies` to discover available attribute slugs. **Example `values`:** ```json 1 { 2 "name": [{ "value": "Acme Corp" }], 3 "domains": [{ "domain": "acme.com" }], 4 "description": [{ "value": "Enterprise SaaS company" }] 5 } ``` | Name | Type | Required | Description | | -------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `values` | object | Yes | Attribute values keyed by attribute slug. The `domains` attribute is unique — creating a company with an existing domain will throw a conflict error | ## `attio_list_companies` [Section titled “attio\_list\_companies”](#attio_list_companies) List company records with optional filtering and sorting. Returns paginated results. **Filter example** — companies in a specific industry: ```json 1 { "categories": [{ "value": "Software" }] } ``` | Name | Type | Required | Description | | -------- | ------- | -------- | ------------------------------------------------------ | | `filter` | object | No | Attribute-based filter conditions | | `sorts` | array | No | Array of sort objects with `attribute` and `direction` | | `limit` | integer | No | Number of records per page (default `500`) | | `offset` | integer | No | Records to skip for pagination (default `0`) | ## `attio_get_company` [Section titled “attio\_get\_company”](#attio_get_company) Retrieve a single company record by its `record_id`. Returns all attribute values with full audit metadata. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------- | | `record_id` | string | Yes | UUID of the company record to retrieve | ## `attio_delete_company` [Section titled “attio\_delete\_company”](#attio_delete_company) Permanently delete a company record by its `record_id`. This operation is irreversible. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------ | | `record_id` | string | Yes | UUID of the company record to delete | ## `attio_create_deal` [Section titled “attio\_create\_deal”](#attio_create_deal) Create a new deal record in Attio. Throws an error if a unique attribute conflict is detected. Provide at least one attribute value. Use `attio_list_attributes` on `deals` to discover available slugs. **Example `values`:** ```json 1 { 2 "name": [{ "value": "Acme Enterprise Contract" }], 3 "value": [{ "currency_value": 50000, "currency_code": "USD" }], 4 "stage": [{ "status": "In progress" }], 5 "associated_company": [{ "target_record_id": "" }] 6 } ``` | Name | Type | Required | Description | | -------- | ------ | -------- | ---------------------------------------- | | `values` | object | Yes | Attribute values keyed by attribute slug | ## `attio_list_deals` [Section titled “attio\_list\_deals”](#attio_list_deals) List deal records with optional filtering and sorting. Returns paginated results. | Name | Type | Required | Description | | -------- | ------- | -------- | ------------------------------------------------------ | | `filter` | object | No | Attribute-based filter conditions | | `sorts` | array | No | Array of sort objects with `attribute` and `direction` | | `limit` | integer | No | Number of records per page (default `500`) | | `offset` | integer | No | Records to skip for pagination (default `0`) | ## `attio_get_deal` [Section titled “attio\_get\_deal”](#attio_get_deal) Retrieve a single deal record by its `record_id`. Returns all attribute values with temporal and audit metadata. | Name | Type | Required | Description | | ----------- | ------ | -------- | ----------------------------------- | | `record_id` | string | Yes | UUID of the deal record to retrieve | ## `attio_delete_deal` [Section titled “attio\_delete\_deal”](#attio_delete_deal) Permanently delete a deal record by its `record_id`. This operation is irreversible. | Name | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------- | | `record_id` | string | Yes | UUID of the deal record to delete | ## `attio_create_record` [Section titled “attio\_create\_record”](#attio_create_record) Create a new record for any Attio object type — including `people`, `companies`, `deals`, and custom objects. Use this as a generic alternative to `attio_create_person` / `attio_create_company` when the object type is dynamic. Throws an error if a unique attribute conflict is detected. **Example — create a custom object record:** ```json 1 { 2 "object": "projects", 3 "values": { 4 "name": [{ "value": "Q2 Migration" }], 5 "status": [{ "status": "active" }] 6 } 7 } ``` | Name | Type | Required | Description | | -------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------- | | `object` | string | Yes | Object type slug (e.g., `people`, `companies`, `deals`) or UUID — use `attio_list_objects` to discover available objects | | `values` | object | Yes | Attribute values keyed by attribute slug. Use `attio_list_attributes` on the target object to discover valid slugs and types | ## `attio_list_records` [Section titled “attio\_list\_records”](#attio_list_records) List and filter records for any Attio object type. Returns guaranteed up-to-date data. Prefer this over `attio_search_records` when you need exact filtering by attribute value (e.g., find by email, status, or domain). Use `attio_search_records` for fuzzy, name-based lookups. **Example — filter people by email:** ```json 1 { 2 "object": "people", 3 "filter": { "email_addresses": [{ "email_address": { "$eq": "alice@acme.com" } }] } 4 } ``` | Name | Type | Required | Description | | -------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | `object` | string | Yes | Object type slug or UUID | | `filter` | object | No | Attribute-based filter. Structure depends on attribute type — use `attio_list_attributes` to discover filter syntax per attribute | | `sorts` | array | No | Array of sort objects with `attribute` (slug) and `direction` (`asc`/`desc`) | | `limit` | integer | No | Records per page (default `500`) | | `offset` | integer | No | Records to skip for pagination (default `0`) | ## `attio_search_records` [Section titled “attio\_search\_records”](#attio_search_records) Search records using a fuzzy text query. Returns matching records with their IDs, labels, and key attributes. Best for user-facing search (e.g., “find the company called Acme”). For precise attribute-based filtering, use `attio_list_records` instead. | Name | Type | Required | Description | | -------- | ------- | -------- | ------------------------------------------------------------------------------------------------------- | | `object` | string | Yes | Object type slug (e.g., `people`, `companies`, `deals`) | | `query` | string | Yes | Fuzzy text query matched against names, emails, and domains. Pass an empty string to return all records | | `limit` | integer | No | Number of results to return (default `20`) | | `offset` | integer | No | Results to skip for pagination (default `0`) | ## `attio_get_record` [Section titled “attio\_get\_record”](#attio_get_record) Retrieve a specific record by object type and record ID. Returns the full record including all attribute values with their complete audit trail (`created_by_actor`, `active_from`, `active_until`). | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------ | | `object` | string | Yes | Object type slug or UUID | | `record_id` | string | Yes | UUID of the record to retrieve | ## `attio_delete_record` [Section titled “attio\_delete\_record”](#attio_delete_record) Permanently delete a record by object type and record ID. This action is irreversible. | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------- | | `object` | string | Yes | Object type slug or UUID | | `record_id` | string | Yes | UUID of the record to delete | ## `attio_get_record_attribute_values` [Section titled “attio\_get\_record\_attribute\_values”](#attio_get_record_attribute_values) Retrieve all values for a specific attribute on a record. Useful for inspecting multi-value attributes (e.g., all email addresses on a person) or retrieving the full value history when `show_historic` is set. Not available for COMINT or enriched attributes. | Name | Type | Required | Description | | --------------- | ------- | -------- | ------------------------------------------------------------------------- | | `object` | string | Yes | Object type slug or UUID | | `record_id` | string | Yes | UUID of the record | | `attribute` | string | Yes | Attribute slug or UUID to retrieve values for | | `show_historic` | boolean | No | Set to `true` to include all historical values, not just the current ones | ## `attio_create_task` [Section titled “attio\_create\_task”](#attio_create_task) Create a new task in Attio. Tasks can be linked to one or more CRM records and assigned to workspace members. Only plaintext is supported for task content. **`linked_records` structure:** ```json 1 [{ "target_object": "people", "target_record_id": "" }] ``` **`assignees` structure** — use `attio_list_workspace_members` to get UUIDs: ```json 1 [{ "referenced_actor_type": "workspace-member", "referenced_actor_id": "" }] ``` | Name | Type | Required | Description | | ---------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `content` | string | Yes | Task content in plaintext (max 2000 characters) | | `deadline_at` | string | No | ISO 8601 deadline timestamp including milliseconds and timezone, e.g. `2026-04-01T17:00:00.000Z` | | `is_completed` | boolean | No | Initial completion status (default: `false`) | | `linked_records` | array | No | Records to link this task to — each item needs `target_object` (slug) and `target_record_id` (UUID) | | `assignees` | array | No | Workspace members to assign the task to — each item needs `referenced_actor_type` (`workspace-member`) and `referenced_actor_id` (UUID) | ## `attio_list_tasks` [Section titled “attio\_list\_tasks”](#attio_list_tasks) List tasks in Attio. Filter by linked record to retrieve tasks for a specific contact, company, or deal. Filter by completion status to show only open or completed tasks. | Name | Type | Required | Description | | ------------------ | ------- | -------- | ----------------------------------------------------------------------------------------- | | `linked_object` | string | No | Object type slug to filter tasks by linked record type (e.g., `people`, `companies`) | | `linked_record_id` | string | No | UUID of the record to filter tasks linked to a specific record — use with `linked_object` | | `is_completed` | boolean | No | Filter by completion status: `true` for completed, `false` for open, omit to return all | | `limit` | integer | No | Number of tasks to return (default `20`) | | `offset` | integer | No | Tasks to skip for pagination (default `0`) | ## `attio_get_task` [Section titled “attio\_get\_task”](#attio_get_task) Retrieve a single task by its `task_id`. Returns the task content, deadline, completion status, assignees, and linked records. | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------- | | `task_id` | string | Yes | UUID of the task to retrieve | ## `attio_delete_task` [Section titled “attio\_delete\_task”](#attio_delete_task) Permanently delete a task by its `task_id`. This operation is irreversible. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------- | | `task_id` | string | Yes | UUID of the task to delete | ## `attio_create_note` [Section titled “attio\_create\_note”](#attio_create_note) Create a note on a CRM record (person, company, deal, or custom object). Notes support plaintext or Markdown. Optionally backdate the note using `created_at` or associate it with an existing meeting via `meeting_id`. | Name | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------------------------------------------------------------------- | | `parent_object` | string | Yes | Object type slug the record belongs to (e.g., `people`, `companies`, `deals`) | | `parent_record_id` | string | Yes | UUID of the record to attach the note to | | `title` | string | No | Plaintext title for the note | | `content` | string | No | Note body — plaintext or Markdown depending on `format` | | `format` | string | No | Content format: `plaintext` (default) or `markdown` | | `created_at` | string | No | ISO 8601 timestamp to backdate the note (e.g., `2026-01-15T10:30:00Z`) — defaults to now | | `meeting_id` | string | No | UUID of an existing meeting to associate with this note | ## `attio_list_notes` [Section titled “attio\_list\_notes”](#attio_list_notes) List notes in Attio. Filter by parent object and record to retrieve notes for a specific person, company, or deal. Maximum 50 results per page. | Name | Type | Required | Description | | ------------------ | ------- | -------- | --------------------------------------------------------------------------------------------------- | | `parent_object` | string | No | Object type slug to filter notes by record type — must be provided together with `parent_record_id` | | `parent_record_id` | string | No | UUID of the record to filter notes — must be provided together with `parent_object` | | `limit` | integer | No | Number of notes to return (max `50`, default `10`) | | `offset` | integer | No | Notes to skip for pagination (default `0`) | ## `attio_get_note` [Section titled “attio\_get\_note”](#attio_get_note) Retrieve a single note by its `note_id`. Returns the note’s title, content in both plaintext and Markdown, tags, and creator information. | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------- | | `note_id` | string | Yes | UUID of the note to retrieve | ## `attio_delete_note` [Section titled “attio\_delete\_note”](#attio_delete_note) Permanently delete a note by its `note_id`. This operation is irreversible. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------- | | `note_id` | string | Yes | UUID of the note to delete | ## `attio_create_select_option` [Section titled “attio\_create\_select\_option”](#attio_create_select_option) Adds a new select option to a `select` or `multiselect` attribute on an Attio object or list. | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------------------------------------- | | `attribute` | string | Yes | Attribute slug or UUID of the select/multiselect attribute | | `object` | string | Yes | Object slug or UUID (e.g. `people`, `companies`, `deals`) | | `title` | string | Yes | Display title for the new select option | ## `attio_create_status` [Section titled “attio\_create\_status”](#attio_create_status) Adds a new status to a status attribute on an Attio object or list. Company and person objects do not support status attributes. | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------------------------- | | `attribute` | string | Yes | Attribute slug or UUID of the status attribute | | `object` | string | Yes | Object slug or UUID (e.g. `deals`) | | `title` | string | Yes | Display title for the new status | ## `attio_create_user_record` [Section titled “attio\_create\_user\_record”](#attio_create_user_record) Creates a new user record in Attio. Requires `primary_email_address`, `user_id`, and `workspace_id` in the values field. Optionally link to an existing person record. | Name | Type | Required | Description | | -------- | ------ | -------- | ---------------------------------------- | | `values` | object | Yes | Attribute values for the new user record | ## `attio_create_webhook` [Section titled “attio\_create\_webhook”](#attio_create_webhook) Creates a webhook and subscribes to events in Attio. Returns the webhook configuration including a one-time signing secret for verifying event authenticity. | Name | Type | Required | Description | | --------------- | ------ | -------- | --------------------------------------------------------------------------------- | | `target_url` | string | Yes | HTTPS URL that will receive webhook event payloads | | `subscriptions` | array | Yes | Array of event subscription objects (each needs `event_type` and `filter` fields) | | `secret` | string | No | Optional signing secret for verifying webhook payloads | ## `attio_create_workspace_record` [Section titled “attio\_create\_workspace\_record”](#attio_create_workspace_record) Creates a new workspace record in Attio. The `workspace_id` field is required and must be unique. Throws an error on conflicts of unique attributes. | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------- | | `values` | object | Yes | Attribute values for the new workspace record | ## `attio_create_comment` [Section titled “attio\_create\_comment”](#attio_create_comment) Create a comment on a CRM record or list entry. To comment on a record, provide `record_object` and `record_id`. To comment on a list entry, provide `list_id` and `entry_id`. To reply to an existing thread, include `thread_id`. Content is always plaintext. Use `attio_list_workspace_members` to look up the `author_actor_id` UUID. | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------- | | `author_actor_id` | string | Yes | UUID of the workspace member authoring the comment — retrieve with `attio_list_workspace_members` | | `content` | string | Yes | Comment content in plaintext | | `record_object` | string | No | Object type slug when commenting on a record (e.g., `people`, `companies`) | | `record_id` | string | No | UUID of the record to comment on | | `list_id` | string | No | UUID of the list when commenting on a list entry | | `entry_id` | string | No | UUID of the list entry to comment on | | `thread_id` | string | No | UUID of an existing comment thread to reply to — omit to start a new thread | ## `attio_get_comment` [Section titled “attio\_get\_comment”](#attio_get_comment) Retrieve a single comment by its `comment_id`. Returns the comment content, author, thread ID, and resolution status. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------- | | `comment_id` | string | Yes | UUID of the comment to retrieve | ## `attio_delete_comment` [Section titled “attio\_delete\_comment”](#attio_delete_comment) Permanently delete a comment by its `comment_id`. If the comment is the head of a thread, all replies in that thread are also deleted. | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------- | | `comment_id` | string | Yes | UUID of the comment to delete | ## `attio_list_threads` [Section titled “attio\_list\_threads”](#attio_list_threads) List all comment threads on a record or list entry. Returns all threads with their messages and resolution status. | Name | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------ | | `record_object` | string | No | Object type slug when listing threads for a record | | `record_id` | string | No | UUID of the record | | `list_id` | string | No | UUID of the list when listing threads for a list entry | | `entry_id` | string | No | UUID of the list entry | ## `attio_list_lists` [Section titled “attio\_list\_lists”](#attio_list_lists) Retrieve all CRM lists in the Attio workspace. Lists track pipeline stages, outreach targets, or custom groupings of records. Optionally filter returned entries by a specific parent record. | Name | Type | Required | Description | | ------------------ | ------ | -------- | ----------------------------------------------------------------------- | | `parent_record_id` | string | No | Filter list entries to those belonging to this record UUID | | `parent_object` | string | No | Object type slug to scope entry filtering (use with `parent_record_id`) | ## `attio_create_list` [Section titled “attio\_create\_list”](#attio_create_list) Create a new CRM list in Attio. After creation, add custom attributes with `attio_create_attribute` and add records with `attio_add_to_list`. **`workspace_member_access` structure** — to give a specific member full access: ```json 1 [{ "workspace_member_id": "", "level": "full-access" }] ``` Pass an empty array `[]` to rely solely on `workspace_access` for all members. | Name | Type | Required | Description | | ------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------- | | `name` | string | Yes | Display name for the list | | `api_slug` | string | Yes | URL-safe identifier used in API calls (e.g., `q2-outreach`) | | `workspace_access` | string | Yes | Default access for all workspace members: `full-access`, `read-and-write`, or `read-only` | | `workspace_member_access` | array | Yes | Per-member access overrides — each item needs `workspace_member_id` and `level`. Pass `[]` for uniform access | ## `attio_get_list` [Section titled “attio\_get\_list”](#attio_get_list) Retrieve details of a single list by its UUID or slug, including its name, parent object type, and access configuration. | Name | Type | Required | Description | | ------ | ------ | -------- | --------------------- | | `list` | string | Yes | List UUID or API slug | ## `attio_list_entries` [Section titled “attio\_list\_entries”](#attio_list_entries) List entries in a given Attio list. Returns the records that belong to the list along with any list-specific attribute values (e.g., pipeline stage, entry date). | Name | Type | Required | Description | | -------- | ------- | -------- | --------------------------------------------- | | `list` | string | Yes | List UUID or API slug | | `filter` | object | No | Filter conditions on list-level attributes | | `sorts` | array | No | Sort objects with `attribute` and `direction` | | `limit` | integer | No | Number of entries to return | | `offset` | integer | No | Entries to skip for pagination | ## `attio_get_list_entry` [Section titled “attio\_get\_list\_entry”](#attio_get_list_entry) Retrieve a single list entry by its `entry_id`. Returns detailed information about the entry including list-specific attribute values. | Name | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------- | | `list` | string | Yes | List UUID or API slug | | `entry_id` | string | Yes | UUID of the list entry to retrieve | ## `attio_add_to_list` [Section titled “attio\_add\_to\_list”](#attio_add_to_list) Add a record to a specific Attio list. Returns the newly created list entry with its `entry_id` — save this if you need to remove the entry later. If the record is already in the list, a new entry is created (Attio supports multiple entries per record in the same list). Optionally set list-level attribute values (e.g., pipeline stage) on the entry using `entry_values`. **`entry_values` example** — set stage on add: ```json 1 { "stage": [{ "status": "Qualified" }] } ``` | Name | Type | Required | Description | | -------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------- | | `list` | string | Yes | List UUID or API slug | | `record_id` | string | Yes | UUID of the record to add | | `entry_values` | object | No | Attribute values to set on the list entry itself (not the underlying record). Keys are list attribute slugs | ## `attio_assert_company` [Section titled “attio\_assert\_company”](#attio_assert_company) Creates or updates a company record in Attio using a unique attribute. If a company with the same matching attribute value exists, it is updated; otherwise a new one is created. | Name | Type | Required | Description | | -------------------- | ------ | -------- | ---------------------------------------------------------- | | `matching_attribute` | string | Yes | The attribute slug to match on for upsert (e.g. `domains`) | | `values` | object | Yes | Attribute values for the company record | ## `attio_assert_list_entry` [Section titled “attio\_assert\_list\_entry”](#attio_assert_list_entry) Creates or updates a list entry for a given parent record in Attio. If an entry with the specified parent record exists, it is updated. Otherwise a new entry is created. | Name | Type | Required | Description | | -------------------- | ------ | -------- | ------------------------------------------------------------- | | `list_id` | string | Yes | The unique identifier or slug of the list | | `matching_attribute` | string | Yes | Attribute slug to match on for upsert | | `parent_object` | string | Yes | Object slug of the parent record (e.g. `people`, `companies`) | | `parent_record_id` | string | Yes | The unique identifier of the parent record | | `entry_values` | object | No | Optional attribute values for the list entry | ## `attio_assert_person` [Section titled “attio\_assert\_person”](#attio_assert_person) Creates or updates a person record in Attio using a unique attribute. If a person with the same matching attribute value exists, it is updated; otherwise a new one is created. | Name | Type | Required | Description | | -------------------- | ------ | -------- | ------------------------------------------------------------------ | | `matching_attribute` | string | Yes | The attribute slug to match on for upsert (e.g. `email_addresses`) | | `values` | object | Yes | Attribute values for the person record | ## `attio_assert_record` [Section titled “attio\_assert\_record”](#attio_assert_record) Trigger an Attio automation workflow for a specific record by upserting the record, which fires any automations configured on record create or update events. Matches on a unique attribute to avoid duplicates. | Name | Type | Required | Description | | -------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------- | | `matching_attribute` | string | Yes | The attribute slug to use as the unique match key (e.g. `email_addresses` for people, `domains` for companies) | | `object` | string | Yes | The slug or UUID of the object type to upsert into (e.g. `people`, `companies`, `deals`) | | `values` | object | Yes | Attribute values for the record | ## `attio_assert_user_record` [Section titled “attio\_assert\_user\_record”](#attio_assert_user_record) Creates or updates a user record in Attio using a unique attribute. If a user with the same matching attribute exists, it is updated; otherwise a new one is created. | Name | Type | Required | Description | | -------------------- | ------ | -------- | ------------------------------------------------------------------------ | | `matching_attribute` | string | Yes | The attribute slug to match on for upsert (e.g. `primary_email_address`) | | `values` | object | Yes | Attribute values for the user record | ## `attio_assert_workspace` [Section titled “attio\_assert\_workspace”](#attio_assert_workspace) Creates or updates a workspace record in Attio using a unique attribute. If a workspace with the same matching attribute value exists, it is updated; otherwise a new one is created. | Name | Type | Required | Description | | -------------------- | ------ | -------- | --------------------------------------------------------------- | | `matching_attribute` | string | Yes | The attribute slug to match on for upsert (e.g. `workspace_id`) | | `values` | object | Yes | Attribute values for the workspace record | ## `attio_remove_from_list` [Section titled “attio\_remove\_from\_list”](#attio_remove_from_list) Remove a specific entry from an Attio list by its `entry_id`. Deletes the list entry but does not delete the underlying record. Use the `entry_id` returned by `attio_add_to_list` or `attio_list_record_entries`. | Name | Type | Required | Description | | ---------- | ------ | -------- | -------------------------------------------------------------------------- | | `list` | string | Yes | List UUID or API slug | | `entry_id` | string | Yes | UUID of the list entry to remove — this is the entry ID, not the record ID | ## `attio_list_record_entries` [Section titled “attio\_list\_record\_entries”](#attio_list_record_entries) List all list memberships for a specific record across all lists. Returns the list IDs, entry IDs, and creation timestamps — useful for finding the `entry_id` needed to remove a record from a list. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------ | | `object` | string | Yes | Object type slug or UUID | | `record_id` | string | Yes | UUID of the record | ## `attio_patch_record` [Section titled “attio\_patch\_record”](#attio_patch_record) Updates a record by its `record_id` using PATCH for any object type. For multiselect attributes, values are prepended to existing values. Use `attio_put_record` to overwrite or remove multiselect values. | Name | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------- | | `object` | string | Yes | Object slug or UUID (e.g. `people`, `companies`, `deals`) | | `record_id` | string | Yes | The unique identifier of the record to update | | `values` | object | Yes | Attribute values to update on the record | ## `attio_put_record` [Section titled “attio\_put\_record”](#attio_put_record) Updates a record by its `record_id` using PUT for any object type. For multiselect attributes, values overwrite existing values. Use `attio_patch_record` to append without removing. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------------------------ | | `object` | string | Yes | Object slug or UUID (e.g. `people`, `companies`, `deals`) | | `record_id` | string | Yes | The unique identifier of the record to update | | `values` | object | Yes | Attribute values to set on the record (overwrites existing multiselect values) | ## `attio_query_records` [Section titled “attio\_query\_records”](#attio_query_records) Queries records for a specific Attio object using server-side filtering operators and sorting. Use for complex filter criteria rather than simple listing. | Name | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------------------------------------ | | `object` | string | Yes | Object slug or UUID to query records for (e.g. `companies`, `people`, `deals`) | | `filter` | object | No | Filter criteria for the query | | `sorts` | array | No | Sorting criteria for the results | | `limit` | number | No | Maximum number of records to return | | `offset` | number | No | Number of records to skip for pagination | ## `attio_list_objects` [Section titled “attio\_list\_objects”](#attio_list_objects) Retrieve all objects available in the Attio workspace — both system objects (`people`, `companies`, `deals`, `users`, `workspaces`) and any custom objects. Call this first to discover what object types exist before querying or writing records. This tool takes no input parameters. ## `attio_get_object` [Section titled “attio\_get\_object”](#attio_get_object) Retrieve details of a single object by its slug or UUID, including its display name, API slug, and whether it is system-defined or custom. | Name | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------- | | `object` | string | Yes | Object slug (e.g., `people`, `companies`) or UUID | ## `attio_create_object` [Section titled “attio\_create\_object”](#attio_create_object) Create a new custom object type in the Attio workspace. Use this when you need an object beyond the standard types. After creation, add attributes with `attio_create_attribute`. | Name | Type | Required | Description | | --------------- | ------ | -------- | -------------------------------------------------------- | | `singular_noun` | string | Yes | Singular display name (e.g., `Project`) | | `plural_noun` | string | Yes | Plural display name (e.g., `Projects`) | | `api_slug` | string | Yes | URL-safe identifier used in API calls (e.g., `projects`) | ## `attio_list_attributes` [Section titled “attio\_list\_attributes”](#attio_list_attributes) List the attribute schema for an object or list — including slugs, types, and configuration for `select`/`status` attributes. **Call this before filtering or writing** to confirm the correct slugs and understand the expected value structure for each attribute type. | Name | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------------------------------ | | `object` | string | No | Object slug or UUID to list attributes for (e.g., `people`, `companies`) | | `list` | string | No | List slug or UUID to list attributes for | ## `attio_get_attribute` [Section titled “attio\_get\_attribute”](#attio_get_attribute) Retrieve full details of a single attribute including its type, slug, configuration, required/unique flags, and metadata. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------- | | `object` | string | No | Object slug or UUID containing the attribute | | `list` | string | No | List slug or UUID containing the attribute | | `attribute` | string | Yes | Attribute slug or UUID | ## `attio_create_attribute` [Section titled “attio\_create\_attribute”](#attio_create_attribute) Create a new attribute on an object or list. The required `type` determines the structure of the `config` object. **Supported `type` values:** `text`, `number`, `select`, `multiselect`, `status`, `date`, `timestamp`, `checkbox`, `currency`, `record-reference`, `actor-reference`, `location`, `domain`, `email-address`, `phone-number`, `interaction` **`config` by type:** * Most types (text, number, date, checkbox, etc.): pass `{}` * `select` / `multiselect`: pass `{}` — add options via `attio_list_attribute_options` after creation * `record-reference`: pass `{ "relationship": { "object": "" } }` | Name | Type | Required | Description | | ---------------- | ------- | -------- | ------------------------------------------------------------------------------- | | `object` | string | No | Object slug or UUID to add the attribute to (provide either `object` or `list`) | | `list` | string | No | List slug or UUID to add the attribute to | | `api_slug` | string | Yes | URL-safe identifier for the attribute — must be unique within the object | | `title` | string | Yes | Display name shown in the Attio UI | | `type` | string | Yes | Attribute data type — see supported values above | | `description` | string | Yes | Human-readable description of what this attribute stores | | `is_required` | boolean | Yes | Whether a value is required when creating records of this type | | `is_unique` | boolean | Yes | Whether values must be unique across all records | | `is_multiselect` | boolean | Yes | Whether multiple values are allowed per record | | `config` | object | Yes | Type-specific configuration — pass `{}` for most types; see examples above | ## `attio_list_attribute_options` [Section titled “attio\_list\_attribute\_options”](#attio_list_attribute_options) List all select options for a `select` or `multiselect` attribute. Returns option IDs, titles, and color configuration. Use this before writing to a select attribute to confirm valid option values. | Name | Type | Required | Description | | ----------- | ------ | -------- | ----------------------------------------------------------------- | | `object` | string | No | Object slug or UUID containing the attribute | | `list` | string | No | List slug or UUID containing the attribute | | `attribute` | string | Yes | Attribute slug or UUID of the `select` or `multiselect` attribute | ## `attio_list_attribute_statuses` [Section titled “attio\_list\_attribute\_statuses”](#attio_list_attribute_statuses) List all statuses for a `status` attribute, including their IDs, titles, and celebration configuration. Use this before writing to a status attribute to confirm valid status titles. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------ | | `object` | string | No | Object slug or UUID containing the attribute | | `list` | string | No | List slug or UUID containing the attribute | | `attribute` | string | Yes | Attribute slug or UUID of the `status` attribute | ## `attio_list_workspace_members` [Section titled “attio\_list\_workspace\_members”](#attio_list_workspace_members) List all workspace members. Use this to retrieve member UUIDs needed when assigning task owners, setting `actor-reference` attributes, or identifying comment authors. | Name | Type | Required | Description | | -------- | ------- | -------- | ------------------------------ | | `limit` | integer | No | Number of members to return | | `offset` | integer | No | Members to skip for pagination | ## `attio_get_workspace_member` [Section titled “attio\_get\_workspace\_member”](#attio_get_workspace_member) Retrieve a single workspace member by their UUID. Returns name, email address, access level, and avatar information. | Name | Type | Required | Description | | --------------------- | ------ | -------- | ---------------------------------------- | | `workspace_member_id` | string | Yes | UUID of the workspace member to retrieve | ## `attio_list_user_records` [Section titled “attio\_list\_user\_records”](#attio_list_user_records) List user records in Attio with optional filtering and sorting. User records represent end-users of the connected product and are distinct from workspace members. Returns paginated results. | Name | Type | Required | Description | | -------- | ------- | -------- | ------------------------------------------------------ | | `filter` | object | No | Attribute-based filter conditions | | `sorts` | array | No | Array of sort objects with `attribute` and `direction` | | `limit` | integer | No | Number of records to return | | `offset` | integer | No | Records to skip for pagination | ## `attio_delete_user_record` [Section titled “attio\_delete\_user\_record”](#attio_delete_user_record) Permanently delete a user record by its `record_id`. This operation is irreversible. | Name | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------- | | `record_id` | string | Yes | UUID of the user record to delete | ## `attio_list_workspace_records` [Section titled “attio\_list\_workspace\_records”](#attio_list_workspace_records) List workspace records in Attio with optional filtering and sorting. Workspace records represent instances of connected SaaS products (e.g., a Slack workspace). Returns paginated results. | Name | Type | Required | Description | | -------- | ------- | -------- | ------------------------------------------------------ | | `filter` | object | No | Attribute-based filter conditions | | `sorts` | array | No | Array of sort objects with `attribute` and `direction` | | `limit` | integer | No | Number of records to return | | `offset` | integer | No | Records to skip for pagination | ## `attio_get_workspace_record` [Section titled “attio\_get\_workspace\_record”](#attio_get_workspace_record) Retrieve a single workspace record by its `record_id`. Returns all attribute values with temporal and audit metadata. | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------------------- | | `record_id` | string | Yes | UUID of the workspace record to retrieve | ## `attio_delete_workspace_record` [Section titled “attio\_delete\_workspace\_record”](#attio_delete_workspace_record) Permanently delete a workspace record by its `record_id`. This operation is irreversible. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------- | | `record_id` | string | Yes | UUID of the workspace record to delete | ## `attio_list_webhooks` [Section titled “attio\_list\_webhooks”](#attio_list_webhooks) Retrieve all webhooks configured in the Attio workspace. Returns webhook configurations, event subscriptions, and statuses. | Name | Type | Required | Description | | -------- | ------- | -------- | ------------------------------- | | `limit` | integer | No | Number of webhooks to return | | `offset` | integer | No | Webhooks to skip for pagination | ## `attio_get_webhook` [Section titled “attio\_get\_webhook”](#attio_get_webhook) Retrieve a single webhook by its `webhook_id`. Returns the target URL, subscribed event types, current status, and metadata. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------- | | `webhook_id` | string | Yes | UUID of the webhook to retrieve | ## `attio_delete_webhook` [Section titled “attio\_delete\_webhook”](#attio_delete_webhook) Permanently delete a webhook by its `webhook_id`. This operation is irreversible. | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------- | | `webhook_id` | string | Yes | UUID of the webhook to delete | ## `attio_list_meetings` [Section titled “attio\_list\_meetings”](#attio_list_meetings) List all meetings in the Attio workspace. Optionally filter by participants or linked records. Caution This tool is in beta and may change without notice. | Name | Type | Required | Description | | -------- | ------- | -------- | ------------------------------------------------------------------- | | `filter` | object | No | Filter conditions (e.g., by participant UUIDs or linked record IDs) | | `limit` | integer | No | Number of meetings to return | | `offset` | integer | No | Meetings to skip for pagination | ## `attio_get_current_token_info` [Section titled “attio\_get\_current\_token\_info”](#attio_get_current_token_info) Identify the current access token, the workspace it belongs to, the actor that authenticated, and the OAuth scopes granted. Use this to verify a user’s token is active, confirm which workspace is connected, and check permissions before making write calls. This tool takes no input parameters. It returns: | Field | Type | Description | | ---------------- | ------- | ---------------------------------------------------------------------------------------------- | | `active` | boolean | Whether the token is currently active and usable | | `workspace.id` | string | UUID of the connected Attio workspace | | `workspace.name` | string | Display name of the connected Attio workspace | | `actor.type` | string | Who authenticated — `workspace-member` for a user OAuth token, `api-token` for a service token | | `actor.id` | string | UUID of the workspace member or API token | | `actor.name` | string | Display name of the authenticated actor | | `actor.email` | string | Email address of the workspace member (present for `workspace-member` actors only) | | `scopes` | array | OAuth scopes granted to this token (e.g., `record_permission:read`, `note:read-write`) | **When to call this:** * After a user completes OAuth — confirm the connection succeeded and identify which workspace they authorized. * Before a write operation — verify the required scope is in `scopes` to give a clear error rather than a cryptic API rejection. * When debugging — check `active` is `true` and that expected scopes are present. ## `attio_update_attribute` [Section titled “attio\_update\_attribute”](#attio_update_attribute) Updates an existing attribute on an Attio object or list by its slug or ID. Can modify title, description, or archive the attribute. Cannot modify system attributes. | Name | Type | Required | Description | | ------------- | ------- | -------- | --------------------------------------------------------- | | `attribute` | string | Yes | Attribute slug or UUID to update | | `object` | string | Yes | Object slug or UUID (e.g. `people`, `companies`, `deals`) | | `title` | string | No | New display title for the attribute | | `is_archived` | boolean | No | Whether to archive the attribute | ## `attio_update_company` [Section titled “attio\_update\_company”](#attio_update_company) Updates a company record in Attio by its `record_id` using PATCH. Only the provided attributes are updated. For multiselect attributes, values are prepended. Note: `logo_url` cannot be updated via API. | Name | Type | Required | Description | | ----------- | ------ | -------- | ----------------------------------------------------- | | `record_id` | string | Yes | The unique identifier of the company record to update | | `values` | object | Yes | Attribute values to update on the company record | ## `attio_update_deal` [Section titled “attio\_update\_deal”](#attio_update_deal) Updates a deal record in Attio by its `record_id` using PATCH. Only the provided fields are updated, leaving other fields unchanged. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------------- | | `record_id` | string | Yes | The unique identifier of the deal record to update | | `values` | object | Yes | Attribute values to update on the deal record | ## `attio_update_list` [Section titled “attio\_update\_list”](#attio_update_list) Updates an existing list in Attio. Can modify name, api\_slug, or permissions. Changing the parent object of a list is not possible through the API. | Name | Type | Required | Description | | ------------------ | ------ | -------- | --------------------------------------------------- | | `list_id` | string | Yes | The unique identifier or slug of the list to update | | `name` | string | No | New display name for the list | | `api_slug` | string | No | New snake\_case identifier for the list | | `workspace_access` | string | No | New access level for workspace members | ## `attio_update_list_entry` [Section titled “attio\_update\_list\_entry”](#attio_update_list_entry) Updates a list entry by its `entry_id` in Attio using PATCH. For multiselect attributes, values are prepended to existing values. Use PUT to overwrite multiselect values. | Name | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------------------- | | `list_id` | string | Yes | The unique identifier or slug of the list | | `entry_id` | string | Yes | The unique identifier of the list entry to update | | `entry_values` | object | Yes | Attribute values to update on the list entry | ## `attio_update_object` [Section titled “attio\_update\_object”](#attio_update_object) Updates a single object’s configuration in Attio. Can modify the object’s API slug, singular noun, or plural noun. | Name | Type | Required | Description | | --------------- | ------ | -------- | ----------------------------------------- | | `object` | string | Yes | Object slug or UUID to update | | `singular_noun` | string | No | New singular noun for the object type | | `plural_noun` | string | No | New plural noun for the object type | | `api_slug` | string | No | New snake\_case identifier for the object | ## `attio_update_person` [Section titled “attio\_update\_person”](#attio_update_person) Updates a person record in Attio by its `record_id` using PATCH. Only the provided attributes are updated. For multiselect attributes, values are prepended. Note: `avatar_url` cannot be updated via API. | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------------------------------- | | `record_id` | string | Yes | The unique identifier of the person record to update | | `values` | object | Yes | Attribute values to update on the person record | ## `attio_update_record` [Section titled “attio\_update\_record”](#attio_update_record) Update an existing record’s attributes in Attio. For multiselect attributes, the supplied values will overwrite (replace) the existing list. Supports people, companies, deals, and custom objects. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------------------- | | `object` | string | Yes | The slug or UUID of the object type (e.g. `people`, `companies`, `deals`) | | `record_id` | string | Yes | The UUID of the record to update | | `values` | object | Yes | Attribute values to update (multiselect values replace all existing) | ## `attio_update_select_option` [Section titled “attio\_update\_select\_option”](#attio_update_select_option) Updates an existing select option for a `select` or `multiselect` attribute in Attio. Can rename or archive an option. Archived options are hidden from selection but preserve historical data. | Name | Type | Required | Description | | ------------- | ------- | -------- | ---------------------------------------------------------- | | `attribute` | string | Yes | Attribute slug or UUID of the select/multiselect attribute | | `object` | string | Yes | Object slug or UUID (e.g. `people`, `companies`, `deals`) | | `option_id` | string | Yes | The unique identifier of the select option to update | | `title` | string | No | New display title for the select option | | `is_archived` | boolean | No | Whether to archive the select option | ## `attio_update_status` [Section titled “attio\_update\_status”](#attio_update_status) Updates a status on a status attribute in Attio. Can modify title or archive the status. Company and person objects do not support status attributes. | Name | Type | Required | Description | | ------------- | ------- | -------- | ---------------------------------------------- | | `attribute` | string | Yes | Attribute slug or UUID of the status attribute | | `object` | string | Yes | Object slug or UUID (e.g. `deals`) | | `status_id` | string | Yes | The unique identifier of the status to update | | `title` | string | No | New display title for the status | | `is_archived` | boolean | No | Whether to archive the status | ## `attio_update_task` [Section titled “attio\_update\_task”](#attio_update_task) Updates an existing task in Attio by its `task_id`. Only deadline, is\_completed, linked\_records, and assignees can be updated via this endpoint. | Name | Type | Required | Description | | ---------------- | ------- | -------- | ---------------------------------------------- | | `task_id` | string | Yes | The unique identifier of the task to update | | `deadline_at` | string | No | ISO 8601 datetime string for the task deadline | | `is_completed` | boolean | No | Whether the task is completed | | `linked_records` | array | No | Records linked to this task | | `assignees` | array | No | Workspace members assigned to this task | ## `attio_update_user_record` [Section titled “attio\_update\_user\_record”](#attio_update_user_record) Updates a user record in Attio by its `record_id` using PATCH. Only the provided attributes are updated; other fields remain unchanged. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------------- | | `record_id` | string | Yes | The unique identifier of the user record to update | | `values` | object | Yes | Attribute values to update on the user record | ## `attio_update_webhook` [Section titled “attio\_update\_webhook”](#attio_update_webhook) Updates a webhook’s target URL and/or event subscriptions in Attio. Each subscription must be an object with an `event_type` field. | Name | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------ | | `webhook_id` | string | Yes | The unique identifier of the webhook to update | | `target_url` | string | No | New HTTPS URL that will receive webhook events | | `subscriptions` | array | No | New array of event subscription objects to replace current subscriptions | ## `attio_update_workspace_record` [Section titled “attio\_update\_workspace\_record”](#attio_update_workspace_record) Updates a workspace record in Attio by its `record_id` using PATCH. Only the provided attributes are updated. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------- | | `record_id` | string | Yes | The unique identifier of the workspace record to update | | `values` | object | Yes | Attribute values to update on the workspace record | --- # DOCUMENT BOUNDARY --- # Google BigQuery > BigQuery is Google Cloud’s fully-managed enterprise data warehouse for analytics at scale. BigQuery is Google Cloud’s fully-managed enterprise data warehouse for analytics at scale. ![Google BigQuery logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/bigquery.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Google BigQuery connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: Caution Google applications using scopes that permit access to certain user data must complete a verification process. 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Google BigQuery** and click **Create**. Click **Use your own credentials** and copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.K5f9uUcQ.png\&w=1280\&h=832\&dpl=69cce21a4f77360008b1503a) * Navigate to [Google Cloud Console](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project) → **APIs & Services** → **Credentials**. Select **+ Create Credentials**, then **OAuth client ID**. Choose **Web application** from the Application type menu. ![Select Web Application in Google OAuth settings](/.netlify/images?url=_astro%2Foauth-web-app.DC96RwBt.png\&w=1100\&h=460\&dpl=69cce21a4f77360008b1503a) * Under **Authorized redirect URIs**, click **+ Add URI**, paste the redirect URI, and click **Create**. ![Add authorized redirect URI in Google Cloud Console](/.netlify/images?url=_astro%2Fadd-redirect-uri.B87wrMK8.png\&w=1504\&h=704\&dpl=69cce21a4f77360008b1503a) 2. ### Enable the BigQuery API * In [Google Cloud Console](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project), go to **APIs & Services** → **Library**. Search for “BigQuery API” and click **Enable**. ![](/.netlify/images?url=_astro%2Fenable-bigquery-api.B6BUg3wp.png\&w=1398\&h=498\&dpl=69cce21a4f77360008b1503a) 3. ### Get client credentials * Google provides your Client ID and Client Secret after you create the OAuth client ID in step 1. 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from above) * Client Secret (from above) * Permissions (scopes — see [Google API Scopes reference](https://developers.google.com/identity/protocols/oauth2/scopes)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s BigQuery account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with BigQuery in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'bigquery'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize BigQuery:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/bigquery/v2/projects', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "bigquery" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize BigQuery:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/bigquery/v2/projects", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Brave Search > Connect to Brave Search to run privacy-first web, news, and local searches, retrieve AI summaries, get LLM grounding context, and use search-backed chat completions. Connect to Brave Search to run privacy-first web, news, image, video, and local searches; retrieve AI-generated summaries and entity data; get LLM-optimized context for grounding; and use OpenAI-compatible chat completions backed by real-time search results. ![Brave Search logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/brave.svg) Supports authentication: API Key What you can build with this connector | Use case | Tools involved | | -------------------------------- | --------------------------------------------------------------------------------------------- | | **Real-time web research** | `brave_web_search` → feed results into LLM context | | **News monitoring** | `brave_news_search` with `freshness: pd` → summarise headlines | | **LLM grounding** | `brave_llm_context` → pass structured snippets directly to your model | | **Search-augmented chat** | `brave_chat_completions` with conversation history → cited AI answers | | **Local business lookup** | `brave_local_place_search` → `brave_local_pois` → display hours, ratings, address | | **AI summaries with follow-ups** | `brave_web_search` (summary: true) → `brave_summarizer_search` → `brave_summarizer_followups` | **Key concepts:** * **Plan tiers**: Free covers core search. Pro adds `brave_llm_context` and summarizer tools. AI plan adds `brave_chat_completions`. Data for AI plan adds local/POI tools. * **Summarizer two-step**: First call `brave_web_search` with `summary: true` to get a `summarizer.key`, then pass that key to `brave_summarizer_*` tools. * **LLM context vs web search**: `brave_llm_context` returns token-budgeted, snippet-optimised output specifically for grounding — prefer it over raw web search results when feeding an LLM. * **Rate limits**: Free plan allows 1 req/s. Paid plans scale to 20 req/s. Handle `429` errors with backoff. Plan requirements Brave Search API tools require different subscription tiers. The **Free plan** (2,000 queries/month) covers core search tools. AI Summarizer tools require **Pro**. Chat completions require the **AI plan**. Local/POI tools require the **Data for AI plan**. See [api.search.brave.com](https://api.search.brave.com) → **Subscription** for details. ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Brave Search API key with Scalekit so it can authenticate and proxy search requests on behalf of your users. Unlike OAuth connectors, Brave Search uses API key authentication — there is no redirect URI or OAuth flow. 1. ## Get a Brave Search API key * Go to [api.search.brave.com](https://api.search.brave.com) and sign in or create a free account. * In the left sidebar, click **API Keys** → **+ New Key**. Give it a name (e.g., `Agent Auth`) and click **Create**. * Copy the key immediately — it is shown only once. ![Brave Search API dashboard showing API Keys page with existing keys and the New Key button](/.netlify/images?url=_astro%2Fcreate-api-key.Div6KB9R.png\&w=1100\&h=560\&dpl=69cce21a4f77360008b1503a) Choosing the right plan Brave Search API offers several subscription tiers. Make sure your plan covers the tools you intend to use: | Plan | Monthly free quota | Tools included | | --------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | **Free** | 2,000 queries/month, 1 req/s | `brave_web_search`, `brave_news_search`, `brave_image_search`, `brave_video_search`, `brave_suggest_search`, `brave_spellcheck` | | **Base / Pro** | Pay-per-use ($3–$5 / 1,000 queries) | All Free tools + `brave_llm_context`, `brave_summarizer_*` (Pro and above) | | **AI** | Pay-per-use | All Pro tools + `brave_chat_completions` | | **Data for AI** | Pay-per-use | Adds `brave_local_place_search`, `brave_local_pois`, `brave_local_descriptions` | Upgrade your plan at [api.search.brave.com](https://api.search.brave.com) → **Subscription**. 2. ## Create a connection in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Brave Search** and click **Create**. * Note the **Connection name** — you will use this as `connection_name` in your code (e.g., `brave-search`). ![Scalekit connection configuration page for Brave Search showing the connection name and API Key authentication type](/.netlify/images?url=_astro%2Fadd-credentials.7y3NjiJb.png\&w=1000\&h=360\&dpl=69cce21a4f77360008b1503a) 3. ## Add a connected account Connected accounts link a specific user identifier in your system to a Brave Search API key. Add them via the dashboard for testing, or via the Scalekit API in production. **Via dashboard (for testing)** * Open the connection you created and click the **Connected Accounts** tab → **Add account**. * Fill in: * **Your User’s ID** — a unique identifier for this user in your system (e.g., `user_123`) * **API Key** — the Brave Search API key you copied in step 1 * Click **Save**. ![Add connected account form for Brave Search in Scalekit dashboard showing User ID and API Key fields](/.netlify/images?url=_astro%2Fadd-connected-account.DjdRY2vi.png\&w=1000\&h=440\&dpl=69cce21a4f77360008b1503a) **Via API (for production)** * Node.js ```typescript 1 await scalekit.actions.upsertConnectedAccount({ 2 connectionName: 'brave-search', 3 identifier: 'user_123', // your user's unique ID 4 credentials: { api_key: 'BSA...' }, 5 }); ``` * Python ```python 1 scalekit_client.actions.upsert_connected_account( 2 connection_name="brave-search", 3 identifier="user_123", 4 credentials={"api_key": "BSA..."} 5 ) ``` Production usage tip In production, call `upsert_connected_account` (Python) / `upsertConnectedAccount` (Node.js) when a user enters their Brave Search API key — for example, on a settings page in your app. Rate limits and quotas Every API key has plan-specific rate limits. The Free plan allows 1 request/second. Paid plans allow up to 20 req/s depending on your tier. Monitor your usage at [api.search.brave.com](https://api.search.brave.com) → **Usage**. Exceeding your quota returns a `429 Too Many Requests` error. ## Usage [Section titled “Usage”](#usage) Once a connected account is set up, make search API calls through the Scalekit proxy. Scalekit injects the Brave Search API key automatically as the `X-Subscription-Token` header — you never handle credentials in your application code. You can interact with Brave Search in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'brave-search'; // connection name from your Scalekit dashboard 5 const identifier = 'user_123'; // your user's unique identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Web search via Scalekit proxy — no API key needed here 16 const result = await actions.request({ 17 connectionName, 18 identifier, 19 path: '/res/v1/web/search', 20 method: 'GET', 21 queryParams: { q: 'best open source LLM frameworks 2025', count: '5' }, 22 }); 23 console.log(result.data.web.results); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "brave-search" # connection name from your Scalekit dashboard 6 identifier = "user_123" # your user's unique identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Web search via Scalekit proxy — no API key needed here 17 result = actions.request( 18 connection_name=connection_name, 19 identifier=identifier, 20 path="/res/v1/web/search", 21 method="GET", 22 params={"q": "best open source LLM frameworks 2025", "count": 5} 23 ) 24 print(result["web"]["results"]) ``` No OAuth flow needed Brave Search uses API key auth — unlike OAuth connectors, there is no authorization link or redirect flow. Once you call `upsert_connected_account` (Python) / `upsertConnectedAccount` (Node.js), or add an account via the dashboard, your users can make requests immediately. ## Scalekit tools Use `actions.execute_tool()` to call Brave Search tools directly. Scalekit injects credentials automatically — your application code never handles the API key. ### Web search Search the web and retrieve real-time results. Works on all plans including Free. examples/brave\_web\_search.py ```python 1 import os 2 from scalekit.client import ScalekitClient 3 4 scalekit_client = ScalekitClient( 5 client_id=os.environ["SCALEKIT_CLIENT_ID"], 6 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 7 env_url=os.environ["SCALEKIT_ENV_URL"], 8 ) 9 10 # Ensure a connected account exists for this user before making tool calls 11 scalekit_client.actions.get_or_create_connected_account( 12 connection_name="brave-search", 13 identifier="user_123", 14 ) 15 16 # Search for recent articles — freshness "pw" limits results to the past 7 days 17 result = scalekit_client.actions.execute_tool( 18 connection_name="brave-search", 19 identifier="user_123", 20 tool_name="brave_web_search", 21 input={ 22 "q": "open source LLM frameworks 2025", 23 "count": 5, 24 "freshness": "pw", 25 }, 26 ) 27 28 for item in result["web"]["results"]: 29 print(item["title"], item["url"]) ``` ### News search Retrieve recent news articles by topic or keyword. Useful for monitoring a brand, topic, or competitor. examples/brave\_news\_search.py ```python 1 # Fetch the latest news on a topic from the past 24 hours 2 result = scalekit_client.actions.execute_tool( 3 connection_name="brave-search", 4 identifier="user_123", 5 tool_name="brave_news_search", 6 input={ 7 "q": "AI regulation Europe", 8 "count": 10, 9 "freshness": "pd", # pd = past 24 hours 10 }, 11 ) 12 13 for article in result["results"]: 14 print(article["title"], article["age"], article["url"]) ``` ### LLM grounding context Retrieve search results structured specifically for grounding LLM responses. Requires Pro plan. Use `token_budget` to stay within your model’s context window. examples/brave\_llm\_context.py ```python 1 # Get search context sized to fit a 4 000-token budget 2 result = scalekit_client.actions.execute_tool( 3 connection_name="brave-search", 4 identifier="user_123", 5 tool_name="brave_llm_context", 6 input={ 7 "q": "vector database comparison 2025", 8 "count": 5, 9 "token_budget": 4000, 10 }, 11 ) 12 13 # Pass the structured context directly to your LLM 14 grounding_context = result["context"] 15 print(grounding_context) ``` ### AI chat completions grounded in search Get an AI-generated answer backed by real-time Brave Search results, using an OpenAI-compatible interface. Requires AI plan. examples/brave\_chat\_completions.py ```python 1 # Drop-in replacement for OpenAI /v1/chat/completions — results are grounded in live search 2 result = scalekit_client.actions.execute_tool( 3 connection_name="brave-search", 4 identifier="user_123", 5 tool_name="brave_chat_completions", 6 input={ 7 "messages": [ 8 {"role": "user", "content": "What are the latest developments in quantum computing?"} 9 ], 10 "model": "brave/serp-claude-3-5-haiku", 11 }, 12 ) 13 14 print(result["choices"][0]["message"]["content"]) 15 # Each answer includes citations back to source URLs 16 for source in result.get("search_results", []): 17 print(source["title"], source["url"]) ``` ### Local place search and POI details Find nearby businesses or points of interest, then retrieve full details. Requires Data for AI plan. examples/brave\_local\_search.py ```python 1 # Step 1: Find coffee shops near San Francisco city centre 2 places = scalekit_client.actions.execute_tool( 3 connection_name="brave-search", 4 identifier="user_123", 5 tool_name="brave_local_place_search", 6 input={ 7 "q": "specialty coffee", 8 "location": "San Francisco, CA", 9 "count": 5, 10 }, 11 ) 12 13 location_ids = [p["id"] for p in places["results"]] 14 15 # Step 2: Fetch rich details (hours, ratings, address) for those locations 16 # Location IDs expire after ~8 hours — always fetch details in the same session 17 pois = scalekit_client.actions.execute_tool( 18 connection_name="brave-search", 19 identifier="user_123", 20 tool_name="brave_local_pois", 21 input={"ids": location_ids}, 22 ) 23 24 for poi in pois["results"]: 25 print(poi["name"], poi["address"], poi["rating"]) ``` ### AI summary with follow-up queries Get an AI-generated summary for a search query, then surface follow-up questions. Requires Pro plan. examples/brave\_summarizer.py ```python 1 # Step 1: Web search with summary: true to obtain a summarizer key 2 search_result = scalekit_client.actions.execute_tool( 3 connection_name="brave-search", 4 identifier="user_123", 5 tool_name="brave_web_search", 6 input={ 7 "q": "benefits of RAG vs fine-tuning for enterprise LLMs", 8 "summary": True, 9 "count": 5, 10 }, 11 ) 12 13 summarizer_key = search_result["summarizer"]["key"] 14 15 # Step 2: Retrieve the full AI summary using the key 16 summary = scalekit_client.actions.execute_tool( 17 connection_name="brave-search", 18 identifier="user_123", 19 tool_name="brave_summarizer_search", 20 input={"key": summarizer_key, "entity_info": True}, 21 ) 22 23 print(summary["title"]) 24 print(summary["summary"]) 25 26 # Step 3: Get follow-up questions for a conversational search experience 27 followups = scalekit_client.actions.execute_tool( 28 connection_name="brave-search", 29 identifier="user_123", 30 tool_name="brave_summarizer_followups", 31 input={"key": summarizer_key}, 32 ) 33 34 for q in followups["queries"]: 35 print("-", q) ``` ### LangChain integration Use Scalekit’s LangChain helper to load all Brave Search tools into a LangChain agent. The agent selects and calls the right tool automatically based on the user’s query. examples/brave\_langchain\_agent.py ```python 1 import os 2 from langchain.agents import AgentExecutor, create_tool_calling_agent 3 from langchain_anthropic import ChatAnthropic 4 from langchain_core.prompts import ChatPromptTemplate 5 from scalekit.client import ScalekitClient 6 7 scalekit_client = ScalekitClient( 8 client_id=os.environ["SCALEKIT_CLIENT_ID"], 9 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 10 env_url=os.environ["SCALEKIT_ENV_URL"], 11 ) 12 13 # Load all Brave Search tools — Scalekit handles auth for each call 14 tools = scalekit_client.actions.langchain.get_tools( 15 providers=["BRAVE_SEARCH"], 16 identifier="user_123", 17 ) 18 19 llm = ChatAnthropic( 20 model="claude-sonnet-4-6", 21 api_key=os.environ["ANTHROPIC_API_KEY"], 22 ) 23 24 prompt = ChatPromptTemplate.from_messages([ 25 ("system", "You are a helpful research assistant with access to Brave Search tools."), 26 ("human", "{input}"), 27 ("placeholder", "{agent_scratchpad}"), 28 ]) 29 30 agent = create_tool_calling_agent(llm, tools, prompt) 31 agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) 32 33 response = agent_executor.invoke({ 34 "input": ( 35 "Find the top 5 news articles about AI regulation in Europe from the past week " 36 "and give me a one-sentence summary of each." 37 ) 38 }) 39 print(response["output"]) ``` ## Tool list [Section titled “Tool list”](#tool-list) The following tools are available when you connect a Brave Search account. Each tool maps to a Brave Search API endpoint. Required plan is noted per tool. ## `brave_web_search` [Section titled “brave\_web\_search”](#brave_web_search) Search the web using Brave’s privacy-focused search engine. Returns real-time results including web pages, news, videos, images, and rich data. Supports filtering by country, language, recency, and custom re-ranking via Goggles. **Required plan**: Free and above. | Name | Type | Required | Description | | ------------------ | ------- | -------- | -------------------------------------------------------------------------------------------------------------------- | | `q` | string | Yes | Search query. Maximum 400 characters, 50 words. | | `country` | string | No | Country code (ISO 3166-1 alpha-2, e.g., `US`, `GB`, `DE`) to localise results. | | `search_lang` | string | No | Language of search results (ISO 639-1, e.g., `en`, `fr`, `de`). | | `ui_lang` | string | No | UI language for rendering result labels (e.g., `en-US`). | | `count` | integer | No | Number of results per page (1–20, default 20). | | `offset` | integer | No | Pagination offset (0–9). Use with `count` to page through up to 200 results. | | `safesearch` | string | No | Content filter: `off`, `moderate` (default), or `strict`. | | `freshness` | string | No | Recency filter: `pd` (24 h), `pw` (7 days), `pm` (31 days), `py` (1 year), or a date range `YYYY-MM-DDtoYYYY-MM-DD`. | | `text_decorations` | boolean | No | Include bold markers in result snippets for query-term highlighting. | | `spellcheck` | boolean | No | Automatically spellcheck the query before searching. | | `goggles_id` | string | No | URL of a Goggles re-ranking file for custom result ordering. | | `units` | string | No | Unit system for unit-sensitive results: `metric` or `imperial`. | | `extra_snippets` | boolean | No | Return up to 5 additional snippets per result. **Requires Pro plan.** | | `summary` | boolean | No | Request an AI summarizer key in the response for use with `brave_summarizer_*` tools. **Requires Pro plan.** | ## `brave_news_search` [Section titled “brave\_news\_search”](#brave_news_search) Search for recent news articles using Brave Search. Returns results with titles, URLs, snippets, publication dates, and source information. Supports filtering by country, language, and recency. **Required plan**: Free and above. | Name | Type | Required | Description | | ---------------- | ------- | -------- | ------------------------------------------------------------------ | | `q` | string | Yes | News search query. | | `country` | string | No | Country code (ISO 3166-1 alpha-2) to localise news results. | | `search_lang` | string | No | Language of news results (ISO 639-1). | | `count` | integer | No | Number of results (1–20, default 20). | | `offset` | integer | No | Pagination offset (0–4). | | `freshness` | string | No | Recency filter: `pd` (24 h), `pw` (7 days), `pm` (31 days). | | `extra_snippets` | boolean | No | Return additional text snippets per result. **Requires Pro plan.** | | `goggles_id` | string | No | URL of a Goggles re-ranking file. | | `safesearch` | string | No | Content filter: `off`, `moderate` (default), or `strict`. | | `spellcheck` | boolean | No | Automatically spellcheck the query before searching. | ## `brave_image_search` [Section titled “brave\_image\_search”](#brave_image_search) Search for images using Brave Search. Returns image results with thumbnails, source URLs, dimensions, and metadata. **Required plan**: Free and above. | Name | Type | Required | Description | | ------------- | ------- | -------- | --------------------------------------------------------- | | `q` | string | Yes | Image search query. | | `country` | string | No | Country code (ISO 3166-1 alpha-2) to localise results. | | `search_lang` | string | No | Language filter (ISO 639-1). | | `count` | integer | No | Number of image results to return (1–3 per API call). | | `safesearch` | string | No | Content filter: `off`, `moderate` (default), or `strict`. | | `spellcheck` | boolean | No | Automatically spellcheck the query before searching. | ## `brave_video_search` [Section titled “brave\_video\_search”](#brave_video_search) Search for videos using Brave Search. Returns results with titles, URLs, thumbnails, durations, and publisher metadata. **Required plan**: Free and above. | Name | Type | Required | Description | | ------------- | ------- | -------- | --------------------------------------------------------- | | `q` | string | Yes | Video search query. | | `country` | string | No | Country code (ISO 3166-1 alpha-2) to localise results. | | `search_lang` | string | No | Language filter (ISO 639-1). | | `count` | integer | No | Number of results (1–20, default 20). | | `offset` | integer | No | Pagination offset (0–9). | | `freshness` | string | No | Recency filter: `pd`, `pw`, `pm`, or a date range. | | `safesearch` | string | No | Content filter: `off`, `moderate` (default), or `strict`. | | `spellcheck` | boolean | No | Automatically spellcheck the query before searching. | ## `brave_suggest_search` [Section titled “brave\_suggest\_search”](#brave_suggest_search) Get autocomplete search suggestions from Brave Search for a given query prefix. Useful for query completion, exploring related search terms, and building search UIs. **Required plan**: Free and above. | Name | Type | Required | Description | | --------- | ------- | -------- | ---------------------------------------------------------- | | `q` | string | Yes | Query prefix to get autocomplete suggestions for. | | `country` | string | No | Country code (ISO 3166-1 alpha-2) to localise suggestions. | | `count` | integer | No | Number of suggestions to return (1–20, default 5). | ## `brave_spellcheck` [Section titled “brave\_spellcheck”](#brave_spellcheck) Check and correct spelling of a query using Brave Search’s spellcheck engine. Returns suggested corrections for misspelled queries. **Required plan**: Free and above. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------------------------------- | | `q` | string | Yes | The query to spellcheck. | | `country` | string | No | Country code (ISO 3166-1 alpha-2) to apply locale-aware corrections. | ## `brave_llm_context` [Section titled “brave\_llm\_context”](#brave_llm_context) Retrieve real-time web search results structured as grounding context for LLMs. Returns curated snippets, source URLs, titles, and metadata specifically formatted to maximise contextual relevance for AI-generated answers. Supports fine-grained token and snippet budgets to control context size. **Required plan**: Pro and above. | Name | Type | Required | Description | | ---------------- | ------- | -------- | ------------------------------------------------------------------------------------------- | | `q` | string | Yes | Search query to retrieve grounding context for. | | `count` | integer | No | Number of web results to include (1–20, default 5). | | `token_budget` | integer | No | Maximum total tokens for the returned context. Use to fit within your LLM’s context window. | | `snippet_budget` | integer | No | Maximum number of snippets to include across all results. | | `country` | string | No | Country code (ISO 3166-1 alpha-2) to localise results. | | `search_lang` | string | No | Language filter (ISO 639-1). | | `freshness` | string | No | Recency filter: `pd`, `pw`, `pm`, `py`, or date range. | | `safesearch` | string | No | Content filter: `off`, `moderate` (default), or `strict`. | ## `brave_chat_completions` [Section titled “brave\_chat\_completions”](#brave_chat_completions) Get AI-generated answers grounded in real-time Brave Search results using an OpenAI-compatible chat completions interface. Returns summarised, cited answers with source references and token usage statistics. Drop-in replacement for OpenAI `/v1/chat/completions` for search-augmented generation. **Required plan**: AI plan. | Name | Type | Required | Description | | ------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | | `messages` | array | Yes | Conversation history in OpenAI format: `[{"role": "user", "content": "..."}]`. Supported roles: `system`, `user`, `assistant`. | | `model` | string | No | Model identifier (e.g., `brave/serp-claude-3-5-haiku`). Defaults to the plan’s included model. | | `stream` | boolean | No | Stream the response using server-sent events (SSE). Default `false`. | | `country` | string | No | Country code (ISO 3166-1 alpha-2) to localise the underlying search. | | `search_lang` | string | No | Language filter for the underlying search (ISO 639-1). | | `safesearch` | string | No | Content filter applied to the underlying search: `off`, `moderate`, or `strict`. | | `freshness` | string | No | Recency filter for the underlying search results. | ## `brave_local_place_search` [Section titled “brave\_local\_place\_search”](#brave_local_place_search) Search 200M+ Points of Interest (POIs) by geographic centre and radius. Supports searching by coordinates or location name with an optional keyword query. Returns location IDs that you can use with `brave_local_pois` and `brave_local_descriptions` to get full details. **Required plan**: Data for AI plan. | Name | Type | Required | Description | | ---------- | ------- | -------- | ------------------------------------------------------------------------------------- | | `q` | string | No | Keyword to filter POIs (e.g., `coffee shop`, `hospital`). | | `lat` | number | No | Latitude of the search centre (–90 to 90). Use with `lon` instead of `location`. | | `lon` | number | No | Longitude of the search centre (–180 to 180). Use with `lat` instead of `location`. | | `location` | string | No | Human-readable location name (e.g., `San Francisco, CA`). Alternative to `lat`/`lon`. | | `radius` | integer | No | Search radius in metres from the centre point. | | `count` | integer | No | Number of POI results to return (1–20, default 5). | ## `brave_local_pois` [Section titled “brave\_local\_pois”](#brave_local_pois) Fetch detailed Point of Interest data for up to 20 location IDs returned by `brave_local_place_search`. Returns rich local business data including address, phone, opening hours, ratings, and reviews. **Required plan**: Data for AI plan. Location IDs are ephemeral Location IDs returned by `brave_local_place_search` expire after approximately 8 hours. Always fetch POI details in the same session as the search, not from a cached or stored ID. | Name | Type | Required | Description | | ----- | ----- | -------- | ------------------------------------------------------------------------------------------ | | `ids` | array | Yes | List of location IDs from a `brave_local_place_search` response (maximum 20 IDs per call). | ## `brave_local_descriptions` [Section titled “brave\_local\_descriptions”](#brave_local_descriptions) Fetch AI-generated descriptions for locations using IDs from a `brave_local_place_search` response. Returns natural language summaries describing the place, its atmosphere, and what visitors can expect. **Required plan**: Data for AI plan. Location IDs are ephemeral Location IDs expire after approximately 8 hours. Use them promptly after receiving them from `brave_local_place_search`. | Name | Type | Required | Description | | ----- | ----- | -------- | ------------------------------------------------------------------------------------------ | | `ids` | array | Yes | List of location IDs from a `brave_local_place_search` response (maximum 20 IDs per call). | ## `brave_summarizer_search` [Section titled “brave\_summarizer\_search”](#brave_summarizer_search) Retrieve a full AI-generated summary for a summarizer key obtained from a `brave_web_search` response (requires `summary: true` on the web search call). Returns the complete summary with title, content, enrichments, follow-up queries, and entity details. **Required plan**: Pro and above. How to get a summarizer key First call `brave_web_search` with `summary: true`. The response includes a `summarizer.key` field. Pass that key to this tool and the other `brave_summarizer_*` tools. | Name | Type | Required | Description | | ------------- | ------- | -------- | ---------------------------------------------------------------------------------- | | `key` | string | Yes | Summarizer key from the `summarizer.key` field of a `brave_web_search` response. | | `entity_info` | boolean | No | Include entity metadata (people, places, organisations) referenced in the summary. | | `raw` | boolean | No | Return unformatted (raw) summary text without inline citation markers. | ## `brave_summarizer_summary` [Section titled “brave\_summarizer\_summary”](#brave_summarizer_summary) Fetch only the complete AI-generated summary content for a summarizer key. Use when you only need the summary text without enrichments or follow-up data. **Required plan**: Pro and above. | Name | Type | Required | Description | | ----- | ------ | -------- | -------------------------------------------------- | | `key` | string | Yes | Summarizer key from a `brave_web_search` response. | ## `brave_summarizer_enrichments` [Section titled “brave\_summarizer\_enrichments”](#brave_summarizer_enrichments) Fetch enrichment data for a Brave AI summary key. Returns associated images, Q\&A pairs, entity details, and source references that accompany the summary. **Required plan**: Pro and above. | Name | Type | Required | Description | | ----- | ------ | -------- | -------------------------------------------------- | | `key` | string | Yes | Summarizer key from a `brave_web_search` response. | ## `brave_summarizer_followups` [Section titled “brave\_summarizer\_followups”](#brave_summarizer_followups) Fetch suggested follow-up queries for a Brave AI summary key. Useful for building conversational search flows and helping users explore related topics. **Required plan**: Pro and above. | Name | Type | Required | Description | | ----- | ------ | -------- | -------------------------------------------------- | | `key` | string | Yes | Summarizer key from a `brave_web_search` response. | ## `brave_summarizer_entity_info` [Section titled “brave\_summarizer\_entity\_info”](#brave_summarizer_entity_info) Fetch detailed entity metadata for a specific entity mentioned in a Brave AI summary. Returns structured information about people, places, organisations, and concepts referenced in the summary. **Required plan**: Pro and above. | Name | Type | Required | Description | | -------- | ------ | -------- | ---------------------------------------------------------------------------- | | `key` | string | Yes | Summarizer key from a `brave_web_search` response. | | `entity` | string | Yes | Name of the entity to retrieve details for (must be present in the summary). | ## `brave_summarizer_title` [Section titled “brave\_summarizer\_title”](#brave_summarizer_title) Fetch only the title component of a Brave AI summary. Use when you need a short heading for the summary without loading the full content. **Required plan**: Pro and above. | Name | Type | Required | Description | | ----- | ------ | -------- | -------------------------------------------------- | | `key` | string | Yes | Summarizer key from a `brave_web_search` response. | --- # DOCUMENT BOUNDARY --- # Chorus > Connect to Chorus.ai to sync calls, transcripts, conversation intelligence, and analytics. Connect to Chorus.ai to sync calls, transcripts, conversation intelligence, and analytics. ![Chorus logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/chorus.svg) Supports authentication: Basic Auth ## Usage [Section titled “Usage”](#usage) Connect a user’s Chorus account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Chorus in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'chorus'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Chorus:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1/users/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "chorus" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Chorus:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1/users/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Clari Copilot > Connect to Clari Copilot for sales call transcripts, analytics, call data, and insights. Connect to Clari Copilot for sales call transcripts, analytics, call data, and insights. ![Clari Copilot logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/clari.svg) Supports authentication: API Key ## Usage [Section titled “Usage”](#usage) Connect a user’s Clari Copilot account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Clari Copilot in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'clari_copilot'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Clari Copilot:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1/users/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "clari_copilot" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Clari Copilot:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1/users/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # ClickUp > Connect to ClickUp. Manage tasks, projects, workspaces, and team collaboration Connect to ClickUp. Manage tasks, projects, workspaces, and team collaboration ![ClickUp logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/clickup.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the ClickUp connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **ClickUp** and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.B4iIRuDT.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * In ClickUp, click your **Workspace avatar** (lower-left corner) → **Settings** → **Integrations** → **ClickUp API**. * Open your application and paste the copied URI under **Redirect URL(s)**, then save. ![Add redirect URI in ClickUp API settings](/.netlify/images?url=_astro%2Fadd-redirect-uri.WMHm00IX.png\&w=1520\&h=704\&dpl=69cce21a4f77360008b1503a) 2. ### Get client credentials On your ClickUp application page (**Settings** → **Integrations** → **ClickUp API**): ![Get ClickUp Client ID and Client Secret](/.netlify/images?url=_astro%2Fget-credentials.DWAjhAk9.png\&w=840\&h=389\&dpl=69cce21a4f77360008b1503a) * **Client ID** — found under **Client ID** on your app page * **Client Secret** — found under **Client Secret** on your app page 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from your ClickUp app page) * Client Secret (from your ClickUp app page) ![Add credentials for ClickUp in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s ClickUp account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with ClickUp in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'clickup'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize ClickUp:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/api/v2/user', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "clickup" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize ClickUp:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/api/v2/user", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Confluence > Connect to Confluence. Manage spaces, pages, content, and team collaboration Connect to Confluence. Manage spaces, pages, content, and team collaboration ![Confluence logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/confluence.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Confluence connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Confluence** and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.udI-LZnP.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * In the [Atlassian Developer Console](https://developer.atlassian.com/console/myapps/), open your app and go to **Authorization** → **OAuth 2.0 (3LO)** → **Configure**. * Paste the copied URI into the **Callback URL** field and click **Save changes**. ![Add callback URL in Atlassian Developer Console](/.netlify/images?url=_astro%2Fadd-redirect-uri.BUa9ZBvs.png\&w=1296\&h=832\&dpl=69cce21a4f77360008b1503a) 2. ### Get client credentials In the [Atlassian Developer Console](https://developer.atlassian.com/console/myapps/), open your app and go to **Settings**: * **Client ID** — listed under **Client ID** * **Client Secret** — listed under **Secret** 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from your Atlassian app settings) * Client Secret (from your Atlassian app settings) * Permissions (scopes — see [Confluence OAuth scopes reference](https://developer.atlassian.com/cloud/confluence/scopes-for-oauth-2-3LO-and-forge-apps/)) ![Add credentials for Confluence in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Confluence account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. **Don’t worry about the Confluence cloud ID in the path.** Scalekit automatically resolves `{{cloud_id}}` from the connected account’s configuration. For example, a request with `path="/wiki/rest/api/user/current"` will be sent to `https://api.atlassian.com/ex/confluence/a1b2c3d4-e5f6-7890-abcd-ef1234567890/wiki/rest/api/user/current` automatically. You can interact with Confluence in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'confluence'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Confluence:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/wiki/rest/api/user/current', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "confluence" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Confluence:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/wiki/rest/api/user/current", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Discord > Connect to Discord. Read user profiles, list guilds, retrieve member data, check entitlements, and verify OAuth2 authorization details. Connect to Discord. Read user profiles, list guilds, retrieve member data, check entitlements, and verify OAuth2 authorization details. ![Discord logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/discord.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Discord connector so Scalekit handles the OAuth 2.0 (PKCE) flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. 1. ### Create a Discord application * Go to the [Discord Developer Portal](https://discord.com/developers/applications) and sign in with your Discord account. * Click **New Application**, enter a name for your app (e.g., `My Agent`), accept the terms, and click **Create**. 2. ### Set up the OAuth2 redirect URI * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Discord** and click **Create**. Copy the redirect URI shown — it looks like: `https:///sso/v1/oauth//callback` ![](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.DMY61Oaa.png\&w=950\&h=520\&dpl=69cce21a4f77360008b1503a) * Back in the Discord Developer Portal, open your application and go to **OAuth2** in the left sidebar. * Under **Redirects**, click **Add Redirect**, paste the URI from Scalekit, and click **Save Changes**. ![](/.netlify/images?url=_astro%2Fadd-redirect-uri.BZwzwOm-.png\&w=1200\&h=760\&dpl=69cce21a4f77360008b1503a) Redirect URI must match exactly Discord performs an exact string match on the redirect URI. Any mismatch — including a trailing slash — will cause the OAuth flow to fail with an `invalid_redirect_uri` error. 3. ### Copy your credentials * On the **OAuth2** page, copy the **Client ID**. * Click **Reset Secret** to generate a **Client Secret** and copy it immediately. It will not be shown again. 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter the credentials you copied: * **Client ID** * **Client Secret** * **Scopes** — select the scopes your agent needs. Common scopes: | Scope | What it grants | | --------------------------- | -------------------------------------------------------------- | | `identify` | Read basic user profile (username, avatar, discriminator) | | `email` | Read the user’s email address | | `guilds` | List the guilds the user belongs to | | `guilds.members.read` | Read the user’s member data within a guild | | `connections` | Read third-party accounts linked to the user’s Discord profile | | `openid` | Use Discord as an OpenID Connect provider | | `applications.entitlements` | Read premium entitlements for your application | ![](/.netlify/images?url=_astro%2Fadd-credentials.kGzz3Jeo.png\&w=950\&h=260\&dpl=69cce21a4f77360008b1503a) * Click **Save**. Request only the scopes you need Discord displays a consent screen listing every requested scope. Requesting unnecessary scopes reduces user trust and may cause authorization to be denied. ## Usage [Section titled “Usage”](#usage) Connect a user’s Discord account and make API calls on their behalf — Scalekit handles OAuth 2.0 (PKCE), token storage, and refresh automatically. You can interact with Discord in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'discord'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user — send this link to your user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Discord:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Fetch the authenticated user's Discord profile via Scalekit proxy 25 const user = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/api/users/@me', 29 method: 'GET', 30 }); 31 console.log(user); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "discord" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user — present this link to your user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 print("🔗 Authorize Discord:", link_response.link) 22 input("Press Enter after authorizing...") 23 24 # Fetch the authenticated user's Discord profile via Scalekit proxy 25 user = actions.request( 26 connection_name=connection_name, 27 identifier=identifier, 28 path="/api/users/@me", 29 method="GET" 30 ) 31 print(user) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `get_guild_widget_png` [Section titled “get\_guild\_widget\_png”](#get_guild_widget_png) Retrieves a PNG image widget for a Discord guild. Returns a visual representation of the guild widget that can be embedded on external websites. The widget must be enabled in the guild’s server settings under **Server Settings → Widget**. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------------------ | | `guild_id` | string | Yes | The ID of the guild whose widget PNG to retrieve | ## `get_current_user_application_entitlements` [Section titled “get\_current\_user\_application\_entitlements”](#get_current_user_application_entitlements) Retrieves entitlements for the current user for a given application. Use when you need to check what premium offerings or subscriptions the authenticated user has access to. Requires the `applications.entitlements` OAuth2 scope. | Name | Type | Required | Description | | ---------------- | ------- | -------- | ----------------------------------------------------------------- | | `application_id` | string | Yes | The ID of the Discord application to check entitlements for | | `sku_id` | string | No | Filter entitlements by a specific SKU ID | | `before` | string | No | Retrieve entitlements before this entitlement ID (for pagination) | | `after` | string | No | Retrieve entitlements after this entitlement ID (for pagination) | | `limit` | integer | No | Maximum number of entitlements to return (1–100, default 100) | | `guild_id` | string | No | Filter entitlements for a specific guild | | `exclude_ended` | boolean | No | Set to `true` to exclude entitlements that have already ended | ## `get_guild_widget` [Section titled “get\_guild\_widget”](#get_guild_widget) Retrieves the guild widget in JSON format. Returns public information about a Discord guild’s widget including online member count and invite URL. The widget must be enabled in the guild’s server settings. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------------------- | | `guild_id` | string | Yes | The ID of the guild whose widget JSON to retrieve | ## `get_user` [Section titled “get\_user”](#get_user) Retrieve information about a Discord user. With an OAuth Bearer token, pass `@me` as `user_id` to return the authenticated user’s information. With a Bot token, you can query any user by their ID. Returns username, avatar, discriminator, locale, premium status, and email (if the `email` scope is granted). | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------------------------------------- | | `user_id` | string | Yes | The ID of the user to retrieve, or `@me` to get the authenticated user | ## `get_my_user` [Section titled “get\_my\_user”](#get_my_user) Fetches comprehensive profile information for the currently authenticated Discord user, including username, avatar, discriminator, locale, and email if the `email` OAuth2 scope is granted. No additional parameters required — uses the OAuth token of the connected account. *This tool takes no input parameters.* ## `get_invite` [Section titled “get\_invite”](#get_invite) Deprecated This tool is deprecated. Use `resolve_invite` instead for new integrations. Retrieves information about a specific invite code including guild and channel details. | Name | Type | Required | Description | | ----------------- | ------- | -------- | ----------------------------------------------------------------- | | `invite_code` | string | Yes | The Discord invite code to look up (the part after `discord.gg/`) | | `with_counts` | boolean | No | Set to `true` to include approximate member and presence counts | | `with_expiration` | boolean | No | Set to `true` to include expiration date information | ## `get_guild_template` [Section titled “get\_guild\_template”](#get_guild_template) Retrieves information about a Discord guild template using its unique template code. Use when you need to get details about a guild template for creating new servers or auditing template configurations. | Name | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------- | | `template_code` | string | Yes | The unique code of the guild template to retrieve | ## `get_public_keys` [Section titled “get\_public\_keys”](#get_public_keys) Retrieves Discord OAuth2 public keys (JWKS). Use when you need to verify OAuth2 tokens or perform cryptographic signature verification against Discord-issued tokens. *This tool takes no input parameters.* ## `list_my_guilds` [Section titled “list\_my\_guilds”](#list_my_guilds) Lists the current user’s guilds, returning partial data (id, name, icon, owner, permissions, features) for each. Primarily used for displaying server lists or verifying guild memberships. Requires the `guilds` OAuth2 scope. | Name | Type | Required | Description | | ------------- | ------- | -------- | --------------------------------------------------------------- | | `before` | string | No | Retrieve guilds with IDs before this guild ID (for pagination) | | `after` | string | No | Retrieve guilds with IDs after this guild ID (for pagination) | | `limit` | integer | No | Maximum number of guilds to return (1–200, default 200) | | `with_counts` | boolean | No | Set to `true` to include approximate member and presence counts | ## `get_openid_connect_userinfo` [Section titled “get\_openid\_connect\_userinfo”](#get_openid_connect_userinfo) Retrieves OpenID Connect compliant user information for the authenticated user. Returns standardized OIDC claims (`sub`, `email`, `nickname`, `picture`, `locale`, etc.) following the OpenID Connect specification. Requires an OAuth2 access token with the `openid` scope; additional fields require the `identify` and `email` scopes. *This tool takes no input parameters.* ## `get_gateway` [Section titled “get\_gateway”](#get_gateway) Retrieves a valid WebSocket (`wss://`) URL for establishing a Gateway connection to Discord. Use when you need to connect to the Discord Gateway for real-time events. No authentication required. *This tool takes no input parameters.* ## `get_my_guild_member` [Section titled “get\_my\_guild\_member”](#get_my_guild_member) Retrieves the guild member object for the currently authenticated user within a specified guild, including roles, nickname, join date, and avatar. Requires the `guilds.members.read` OAuth2 scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------------------------------------------- | | `guild_id` | string | Yes | The ID of the guild to retrieve the authenticated user’s member data from | ## `list_sticker_packs` [Section titled “list\_sticker\_packs”](#list_sticker_packs) Retrieves all available Discord Nitro sticker packs. Returns official Discord sticker packs including pack name, description, stickers, cover sticker, and banner asset. No authentication required. *This tool takes no input parameters.* ## `resolve_invite` [Section titled “resolve\_invite”](#resolve_invite) Resolves and retrieves information about a Discord invite code, including the associated guild, channel, scheduled event, and inviter. Prefer this over the deprecated `get_invite` tool for new integrations. | Name | Type | Required | Description | | -------------------------- | ------- | -------- | ---------------------------------------------------------------------------- | | `invite_code` | string | Yes | The Discord invite code to resolve (the part after `discord.gg/`) | | `with_counts` | boolean | No | Set to `true` to include approximate member and presence counts | | `with_expiration` | boolean | No | Set to `true` to include expiration date information | | `guild_scheduled_event_id` | string | No | Include details about a specific scheduled event associated with this invite | ## `get_my_oauth2_authorization` [Section titled “get\_my\_oauth2\_authorization”](#get_my_oauth2_authorization) Retrieves current OAuth2 authorization details for the application, including app info, granted scopes, token expiration date, and user data (contingent on scopes like `identify`). Useful for verifying what access the current token has before making downstream API calls. *This tool takes no input parameters.* ## `retrieve_user_connections` [Section titled “retrieve\_user\_connections”](#retrieve_user_connections) Retrieves a list of the authenticated user’s connected third-party accounts on Discord, such as Twitch, YouTube, GitHub, Steam, and others. Returns the service name, account ID, account name, and visibility for each connection. Requires the `connections` OAuth2 scope. *This tool takes no input parameters.* --- # DOCUMENT BOUNDARY --- # Dropbox > Connect to Dropbox. Manage files, folders, sharing, and cloud storage workflows Connect to Dropbox. Manage files, folders, sharing, and cloud storage workflows ![Dropbox logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/drop_box.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Dropbox connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. You’ll need your app credentials from the [Dropbox App Console](https://www.dropbox.com/developers/apps). 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. * Find **Dropbox** from the list of providers and click **Create**. Note By default, a connection using Scalekit’s credentials will be created. If you are testing, go directly to the next section. Before going to production, update your connection by following the steps below. * Click **Use your own credentials** and copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.CNc7Sqjq.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * In the [Dropbox App Console](https://www.dropbox.com/developers/apps), open your app and go to the **Settings** tab. * Under **Redirect URIs**, paste the copied URI and click **Add**. ![Add redirect URI in Dropbox App Console](/.netlify/images?url=_astro%2Fadd-redirect-uri.ChT3NDRf.png\&w=1440\&h=820\&dpl=69cce21a4f77360008b1503a) 2. ### Get client credentials * In the [Dropbox App Console](https://www.dropbox.com/developers/apps), open your app and go to the **Settings** tab: * **Client ID** — listed under **App key** * **Client Secret** — listed under **App secret** 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (App key from your Dropbox app) * Client Secret (App secret from your Dropbox app) * Permissions — select the scopes your app needs ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Dropbox account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Dropbox in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'dropbox'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Dropbox:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/2/users/get_current_account', 29 method: 'POST', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "dropbox" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Dropbox:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/2/users/get_current_account", 30 method="POST" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # evertrace.ai > Connect to evertrace.ai to search and manage talent signals, saved searches, and lists Connect to evertrace.ai to search and manage talent signals, saved searches, and lists. Access rich professional profiles with scoring, experiences, and education data to power your recruiting and sourcing workflows. ![evertrace.ai logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/evertrace.png) Supports authentication: API Key ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your evertrace.ai API key with Scalekit so it can authenticate and proxy requests on behalf of your users. 1. ### Generate an evertrace.ai API key [Section titled “Generate an evertrace.ai API key”](#generate-an-evertraceai-api-key) * Sign in to evertrace.ai. Go to **Settings** → **API Keys**. * Create a new API key and copy it. Store it somewhere safe — you will not be able to view it again. 2. ### Create a connection in Scalekit [Section titled “Create a connection in Scalekit”](#create-a-connection-in-scalekit) * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **evertrace.ai** and click **Create**. * Note the **Connection name** — you will use this as `connection_name` in your code (e.g., `evertrace`). 3. ### Add a connected account [Section titled “Add a connected account”](#add-a-connected-account) Connected accounts link a specific user identifier in your system to an evertrace.ai API key. Add accounts via the dashboard for testing, or via the Scalekit API in production. **Via dashboard (for testing)** * Open the connection you created and click the **Connected Accounts** tab → **Add account**. * Fill in: * **Your User’s ID** — a unique identifier for this user in your system (e.g., `user_123`) * **API Key** — the evertrace.ai API key you copied in step 1 * Click **Save**. **Via API (for production)** * Node.js ```typescript 1 await scalekit.actions.upsertConnectedAccount({ 2 connectionName: 'evertrace', 3 identifier: 'user_123', 4 credentials: { api_key: 'your-evertrace-api-key' }, 5 }); ``` * Python ```python 1 scalekit_client.actions.upsert_connected_account( 2 connection_name="evertrace", 3 identifier="user_123", 4 credentials={"api_key": "your-evertrace-api-key"} 5 ) ``` ## Usage [Section titled “Usage”](#usage) Once a connected account is set up, make API calls through the Scalekit proxy. Scalekit injects the evertrace.ai API key automatically. * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'evertrace'; 5 const identifier = 'user_123'; 6 7 const scalekit = new ScalekitClient( 8 process.env.SCALEKIT_ENV_URL, 9 process.env.SCALEKIT_CLIENT_ID, 10 process.env.SCALEKIT_CLIENT_SECRET 11 ); 12 const actions = scalekit.actions; 13 14 const result = await actions.request({ 15 connectionName, 16 identifier, 17 path: '/signals', 18 method: 'GET', 19 }); 20 console.log(result.data); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "evertrace" 6 identifier = "user_123" 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_ENV_URL"), 12 ) 13 actions = scalekit_client.actions 14 15 result = actions.request( 16 connection_name=connection_name, 17 identifier=identifier, 18 path="/signals", 19 method="GET", 20 ) 21 print(result) ``` ## Tool list [Section titled “Tool list”](#tool-list) ## `evertrace_cities_list` [Section titled “evertrace\_cities\_list”](#evertrace_cities_list) Search available cities by name. Returns city name strings sorted by signal count. Use these values in signal filters for the city field. | Name | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------------------- | | `search` | string | No | Case-insensitive partial match on city name (e.g. “san fran”) | | `page` | string | No | Page number for pagination | | `limit` | string | No | Number of results per page | ## `evertrace_companies_list` [Section titled “evertrace\_companies\_list”](#evertrace_companies_list) Search companies by name or look up by specific IDs. Returns company entity IDs (exe\_\* format) needed for signal filtering by past\_companies. | Name | Type | Required | Description | | -------- | ------ | -------- | -------------------------------------------------------------- | | `search` | string | No | Case-insensitive partial match on company name (e.g. “google”) | | `ids` | array | No | Look up specific companies by entity ID (exe\_\* format) | | `page` | string | No | Page number for pagination | | `limit` | string | No | Number of results per page | ## `evertrace_educations_list` [Section titled “evertrace\_educations\_list”](#evertrace_educations_list) Search education institutions by name or look up by specific IDs. Returns institution entity IDs (ede\_\* format) needed for signal filtering by past\_education. | Name | Type | Required | Description | | -------- | ------ | -------- | -------------------------------------------------------------------- | | `search` | string | No | Case-insensitive partial match on institution name (e.g. “stanford”) | | `ids` | array | No | Look up specific institutions by entity ID (ede\_\* format) | | `page` | string | No | Page number for pagination | | `limit` | string | No | Number of results per page | ## `evertrace_list_entries_create` [Section titled “evertrace\_list\_entries\_create”](#evertrace_list_entries_create) Add a signal to a list. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------- | | `list_id` | string | Yes | The list ID to add the signal to | | `signal_id` | string | Yes | The signal ID to add | ## `evertrace_list_entries_delete` [Section titled “evertrace\_list\_entries\_delete”](#evertrace_list_entries_delete) Remove an entry from a list. | Name | Type | Required | Description | | ---------- | ------ | -------- | ---------------------- | | `list_id` | string | Yes | The list ID | | `entry_id` | string | Yes | The entry ID to remove | ## `evertrace_list_entries_download` [Section titled “evertrace\_list\_entries\_download”](#evertrace_list_entries_download) Export list entries as a CSV file. Maximum 250 entries per export. | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------- | | `list_id` | string | Yes | The list ID to export entries from | ## `evertrace_list_entries_get` [Section titled “evertrace\_list\_entries\_get”](#evertrace_list_entries_get) Get a single list entry with its full signal profile. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------ | | `list_id` | string | Yes | The list ID | | `entry_id` | string | Yes | The entry ID | ## `evertrace_list_entries_list` [Section titled “evertrace\_list\_entries\_list”](#evertrace_list_entries_list) List entries in a list with pagination, sorting, and filtering by screening/viewed status. | Name | Type | Required | Description | | ------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------- | | `list_id` | string | Yes | The list ID | | `page` | string | No | Page number for pagination | | `limit` | string | No | Number of results per page | | `sort_by` | string | No | Sort field: “entry\_created\_at” (when added to list) or “signal\_discovered\_at” (when signal was discovered) | | `sort_order` | string | No | Sort direction: “asc” (oldest first) or “desc” (newest first) | | `screened_by` | array | No | Filter by screening status. Prefix with ”-” to exclude (e.g. \[“-me”, “-others”]) | | `viewed_by` | array | No | Filter by viewed status. Prefix with ”-” to exclude (e.g. \[“-me”]) | ## `evertrace_list_entries_list_by_linkedin_id` [Section titled “evertrace\_list\_entries\_list\_by\_linkedin\_id”](#evertrace_list_entries_list_by_linkedin_id) Get all signals representing the same person as a list entry, matched by LinkedIn ID. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------ | | `list_id` | string | Yes | The list ID | | `entry_id` | string | Yes | The entry ID | ## `evertrace_lists_create` [Section titled “evertrace\_lists\_create”](#evertrace_lists_create) Create a new list. Provide user IDs in accesses to share the list with teammates. The creator is automatically granted access. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------------------------------------------------- | | `name` | string | Yes | Name of the new list | | `accesses` | array | No | Array of user IDs to share this list with. Pass an empty array for private list | ## `evertrace_lists_delete` [Section titled “evertrace\_lists\_delete”](#evertrace_lists_delete) Permanently delete a list and all its entries. | Name | Type | Required | Description | | ---- | ------ | -------- | --------------------- | | `id` | string | Yes | The list ID to delete | ## `evertrace_lists_get` [Section titled “evertrace\_lists\_get”](#evertrace_lists_get) Get a list by ID with its entries, accesses, and creator information. | Name | Type | Required | Description | | ---- | ------ | -------- | ----------- | | `id` | string | Yes | The list ID | ## `evertrace_lists_list` [Section titled “evertrace\_lists\_list”](#evertrace_lists_list) List all lists the current user has access to in evertrace.ai. ## `evertrace_lists_update` [Section titled “evertrace\_lists\_update”](#evertrace_lists_update) Rename a list. | Name | Type | Required | Description | | ------ | ------ | -------- | --------------------- | | `id` | string | Yes | The list ID to update | | `name` | string | Yes | New name for the list | ## `evertrace_screenings_count` [Section titled “evertrace\_screenings\_count”](#evertrace_screenings_count) Count how many signals the current user has screened. ## `evertrace_searches_create` [Section titled “evertrace\_searches\_create”](#evertrace_searches_create) Create a new saved search with filters. Each filter requires a key, operator, and value. Provide sharee user IDs to share the search with teammates. | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------------------------------------------------------------------------- | | `title` | string | Yes | Title of the saved search (max 50 characters) | | `visited_at` | number | Yes | Epoch timestamp in milliseconds for when the search was last visited | | `filters` | array | Yes | Array of filter objects. Each filter has: key (e.g. “country”, “industry”, “score”), operator (e.g. “in”), and value | | `sharees` | array | Yes | Array of user IDs to share this search with | | `emoji` | string | No | Optional emoji for the saved search | ## `evertrace_searches_delete` [Section titled “evertrace\_searches\_delete”](#evertrace_searches_delete) Permanently delete a saved search. | Name | Type | Required | Description | | ---- | ------ | -------- | ----------------------------- | | `id` | string | Yes | The saved search ID to delete | ## `evertrace_searches_duplicate` [Section titled “evertrace\_searches\_duplicate”](#evertrace_searches_duplicate) Duplicate a saved search, creating a copy with the same filters and settings. | Name | Type | Required | Description | | ---- | ------ | -------- | -------------------------------- | | `id` | string | Yes | The saved search ID to duplicate | ## `evertrace_searches_get` [Section titled “evertrace\_searches\_get”](#evertrace_searches_get) Get a saved search by ID with its filters and sharees. | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------- | | `id` | string | Yes | The saved search ID | ## `evertrace_searches_list` [Section titled “evertrace\_searches\_list”](#evertrace_searches_list) List all saved searches accessible to the current user in evertrace.ai. ## `evertrace_searches_notifications_get` [Section titled “evertrace\_searches\_notifications\_get”](#evertrace_searches_notifications_get) Count new signals matching a saved search since it was last visited. Use the optional max parameter to cap the count for performance. | Name | Type | Required | Description | | ----- | ------ | -------- | ----------------------------------------------------- | | `id` | string | Yes | The saved search ID | | `max` | string | No | Optional cap on the count for performance (e.g. “99”) | ## `evertrace_searches_signals_list` [Section titled “evertrace\_searches\_signals\_list”](#evertrace_searches_signals_list) List signals matching a saved search’s filters with pagination. | Name | Type | Required | Description | | ------- | ------ | -------- | -------------------------- | | `id` | string | Yes | The saved search ID | | `page` | string | No | Page number for pagination | | `limit` | string | No | Number of results per page | ## `evertrace_searches_update` [Section titled “evertrace\_searches\_update”](#evertrace_searches_update) Update a saved search. All fields are optional — only provided fields are changed. If filters are provided, they replace all existing filters. If sharees are provided, they replace the full access list. | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------------------------- | | `id` | string | Yes | The saved search ID to update | | `title` | string | No | New title for the saved search (max 50 characters) | | `emoji` | string | No | New emoji for the saved search | | `visited_at` | number | No | Epoch timestamp in milliseconds for when the search was last visited | | `filters` | array | No | Replaces all existing filters. Each filter has: key, operator, value | | `sharees` | array | No | Replaces the full sharee list with these user IDs | ## `evertrace_signal_mark_viewed` [Section titled “evertrace\_signal\_mark\_viewed”](#evertrace_signal_mark_viewed) Mark a signal as viewed by the current user. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------- | | `signal_id` | string | Yes | The ID of the signal to mark as viewed | ## `evertrace_signal_screen` [Section titled “evertrace\_signal\_screen”](#evertrace_signal_screen) Screen a signal, marking it as reviewed by the current user. Screened signals are hidden from default views. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------ | | `signal_id` | string | Yes | The ID of the signal to screen | ## `evertrace_signal_unscreen` [Section titled “evertrace\_signal\_unscreen”](#evertrace_signal_unscreen) Unscreen a signal, making it visible again in default views. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------- | | `signal_id` | string | Yes | The ID of the signal to unscreen | ## `evertrace_signals_count` [Section titled “evertrace\_signals\_count”](#evertrace_signals_count) Count signals matching the given filters without returning the data. Accepts the same filters as the list endpoint. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------- | | `time_range` | array | No | Absolute date range as \[from, to] in YYYY-MM-DD format. Mutually exclusive with time\_relative | | `time_relative` | string | No | Relative time window in days from today (e.g. “30”). Mutually exclusive with time\_range | | `created_after` | string | No | Epoch timestamp in milliseconds. Only counts signals discovered after this point | | `score` | string | No | Minimum score threshold (1-10) | | `fullname` | string | No | Free-text search on person name | | `type` | array | No | Filter by signal type. Valid values: “New Company”, “Stealth Position”, “Left Position”, “New Position”, “Promoted”, etc. | | `country` | array | No | Filter by country. Prefix with ”!” to exclude | | `industry` | array | No | Filter by industry vertical. Prefix with ”!” to exclude | | `origin` | array | No | Filter by nationality/origin country | | `region` | array | No | Filter by geographic region or US state | | `past_companies` | array | No | Filter by past employer using company entity IDs (exe\_\* format) | | `past_education` | array | No | Filter by past education institution using IDs (ede\_\* format) | | `screened_by` | array | No | Filter by screening status. Use “me”, “others”, or user IDs. Prefix with ”-” to exclude | ## `evertrace_signals_download` [Section titled “evertrace\_signals\_download”](#evertrace_signals_download) Export signals matching the given filters as a CSV file. Maximum 250 signals per export. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------------------------------------------------------------------------------------------- | | `time_range` | array | No | Absolute date range as \[from, to] in YYYY-MM-DD format. Mutually exclusive with time\_relative | | `time_relative` | string | No | Relative time window in days from today (e.g. “30”). Mutually exclusive with time\_range | | `score` | string | No | Minimum score threshold (1-10) | | `type` | array | No | Filter by signal type | | `country` | array | No | Filter by country. Prefix with ”!” to exclude | | `industry` | array | No | Filter by industry vertical | | `origin` | array | No | Filter by nationality/origin country | | `past_education` | array | No | Filter by past education institution using IDs (ede\_\* format) | | `page` | string | No | Page number for pagination | | `limit` | string | No | Number of results per page (max 250) | ## `evertrace_signals_entries` [Section titled “evertrace\_signals\_entries”](#evertrace_signals_entries) Get all list entries for a signal. Shows which lists this signal has been added to. | Name | Type | Required | Description | | ---- | ------ | -------- | ------------- | | `id` | string | Yes | The signal ID | ## `evertrace_signals_get` [Section titled “evertrace\_signals\_get”](#evertrace_signals_get) Get a single talent signal by ID with full profile details including experiences, educations, taggings, views, and screenings. | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------------- | | `id` | string | Yes | The signal ID to retrieve | ## `evertrace_signals_list` [Section titled “evertrace\_signals\_list”](#evertrace_signals_list) Search and filter talent signals with pagination. Returns full signal profiles including experiences, educations, taggings, views, and screenings. | Name | Type | Required | Description | | ----------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `time_range` | array | No | Absolute date range as \[from, to] in YYYY-MM-DD format (e.g. \[“2026-01-01”, “2026-03-01”]). Mutually exclusive with time\_relative | | `time_relative` | string | No | Relative time window in days from today (e.g. “30”, “60”, “90”). Mutually exclusive with time\_range | | `created_after` | string | No | Epoch timestamp in milliseconds. Only returns signals discovered after this point | | `score` | string | No | Minimum score threshold (1-10). Acts as a >= filter | | `fullname` | string | No | Free-text search on person name (case-insensitive partial match) | | `profile_tags` | array | No | Filter by profile background tags. Valid values: “Serial Founder”, “VC Backed Founder”, “VC Backed Operator”, “VC Investor”, “YC Alumni”, “Big Tech experience”, “Big 4 experience”, “Banking experience”, “Consulting experience” | | `type` | array | No | Filter by signal type. Valid values: “New Company”, “Stealth Position”, “Left Position”, “Investor Position”, “Board Position”, “New Position”, “Promoted”, “New Patent”, “New Grant” | | `country` | array | No | Filter by country name (e.g. \[“United States”]). Prefix with ”!” to exclude | | `gender` | array | No | Filter by gender. Valid values: “man”, “woman” | | `age` | array | No | Filter by age range buckets. Valid values: “Below 25”, “25 to 29”, “30 to 34”, “35 to 39”, “40 to 44”, “45 to 49”, “Above 49” | | `past_companies` | array | No | Filter by past employer using company entity IDs in exe\_\* format. Use evertrace\_companies\_list to look up IDs | | `past_education` | array | No | Filter by past education institution using IDs in ede\_\* format. Use evertrace\_educations\_list to look up IDs | | `education_level` | array | No | Filter by highest education level. Valid values: “Bachelor”, “Master”, “PhD or Above”, “MBA”, “No university degree” | | `customer_focus` | array | No | Filter by target market. Valid values: “B2B”, “B2C” | | `industry` | array | No | Filter by industry vertical (e.g. \[“Technology”, “Healthcare”]). Prefix with ”!” to exclude | | `origin` | array | No | Filter by nationality/origin country (e.g. \[“India”]). Prefix with ”!” to exclude | | `region` | array | No | Filter by geographic region or US state (e.g. \[“Europe”, “California”]). Prefix with ”!” to exclude | | `city` | array | No | Filter by city name (e.g. \[“San Francisco”]). Use evertrace\_cities\_list to search available cities. Prefix with ”!” to exclude | | `source` | array | No | Filter by data source name. Values are dynamic per workspace | | `screened_by` | array | No | Filter by screening status. Use “me”, “others”, or user IDs. Prefix with ”-” to exclude | | `page` | string | No | Page number for pagination | | `limit` | string | No | Number of results per page | ## `evertrace_signals_list_by_linkedin_id` [Section titled “evertrace\_signals\_list\_by\_linkedin\_id”](#evertrace_signals_list_by_linkedin_id) Get all signals representing the same person, matched by LinkedIn ID. Useful for finding duplicate or historical signals for the same individual. | Name | Type | Required | Description | | ---- | ------ | -------- | --------------------------------------- | | `id` | string | Yes | The signal ID to match LinkedIn ID from | ## `evertrace_views_count` [Section titled “evertrace\_views\_count”](#evertrace_views_count) Count how many signals the current user has viewed. --- # DOCUMENT BOUNDARY --- # Exa > Connect to Exa for AI-powered semantic web search, content enrichment, finding similar pages, website crawling, direct answers, structured research, and large-scale URL discovery. Connect to Exa to perform AI-powered semantic web search, crawl websites for structured content, get natural language answers from the web, run in-depth research, and execute large-scale URL discovery with Websets. ![Exa logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/exa.svg) Supports authentication: API Key What you can build with this connector | Use case | Tools involved | | ----------------------------- | ----------------------------------------------------------------- | | **Semantic lead research** | `exa_search` (category: company) → `exa_get_contents` (summaries) | | **Competitive intelligence** | `exa_find_similar` → `exa_get_contents` → `exa_research` | | **Real-time Q\&A grounding** | `exa_answer` → feed answer + sources into LLM context | | **Bulk URL discovery** | `exa_websets` (thousands of results) → `exa_get_contents` | | **Documentation indexing** | `exa_crawl` (follow internal links) → chunk and embed | | **Structured market reports** | `exa_research` (with `output_schema`) → structured JSON output | **Key concepts:** * **Neural vs keyword search**: `neural` (default) finds conceptually related pages even when your exact words don’t appear. Use `keyword` for precise product names, quotes, or code identifiers. * **Credits**: Every request costs credits. Requesting `text`, `highlights`, or `summary` costs extra per result. Omitting content fields returns metadata only and saves credits. * **`exa_research` vs `exa_search`**: Use `exa_search` for targeted queries. Use `exa_research` when you need multi-angle synthesis across many sources — it runs sub-queries in parallel and costs significantly more. ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) ## Tool list [Section titled “Tool list”](#tool-list) ## `exa_answer` [Section titled “exa\_answer”](#exa_answer) Get a natural language answer to a question by searching the web with Exa and synthesizing results. Returns a direct answer with citations to the source pages. Ideal for factual questions, current events, and research queries. Rate limit: 60 requests/minute. | Name | Type | Required | Description | | ----------------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | | `exclude_domains` | `array` | No | JSON array of domains to exclude from answer sources. | | `include_domains` | `array` | No | JSON array of domains to restrict source search to. Example: \[“reuters.com”,“bbc.com”] | | `include_text` | boolean | No | When true, also returns the source page text alongside the synthesized answer. | | `num_results` | integer | No | Number of web sources to use when generating the answer (1–20). More sources improves accuracy but costs more credits. | | `query` | string | Yes | The question or query to answer from web sources. | ## `exa_crawl` [Section titled “exa\_crawl”](#exa_crawl) Crawl one or more web pages by URL and extract their content including full text, highlights, and AI-generated summaries. Useful for reading specific pages discovered via search. Rate limit: 60 requests/minute. Credit consumption depends on number of URLs. | Name | Type | Required | Description | | -------------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------ | | `highlights_per_url` | integer | No | Number of highlight sentences to return per URL when include\_highlights is true. Defaults to 3. | | `include_highlights` | boolean | No | When true, returns the most relevant sentence-level highlights from each page. | | `include_html_tags` | boolean | No | When true, retains HTML tags in the extracted text. Defaults to false (plain text only). | | `include_summary` | boolean | No | When true, returns an AI-generated summary for each crawled page. | | `max_characters` | integer | No | Maximum characters of text to extract per page. Defaults to 5000. | | `summary_query` | string | No | Optional query to focus the AI summary on a specific aspect of the page. | | `urls` | `array` | Yes | JSON array of URLs to crawl and extract content from. | ## `exa_delete_webset` [Section titled “exa\_delete\_webset”](#exa_delete_webset) Delete an Exa Webset by its ID. This permanently removes the webset and all its collected items. This action cannot be undone. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------- | | `webset_id` | string | Yes | The ID of the webset to delete. | ## `exa_find_similar` [Section titled “exa\_find\_similar”](#exa_find_similar) Find web pages similar to a given URL using Exa’s neural similarity search. Useful for competitor research, finding related articles, or discovering similar companies. Optionally returns page text, highlights, or summaries. Rate limit: 60 requests/minute. | Name | Type | Required | Description | | ---------------------- | --------------- | -------- | -------------------------------------------------------------------------------------------------- | | `end_published_date` | string | No | Only return pages published before this date. ISO 8601 format: YYYY-MM-DDTHH:MM:SS.000Z | | `exclude_domains` | `array` | No | Array of domains to exclude from results. | | `include_domains` | `array` | No | Array of domains to restrict results to. | | `include_text` | boolean | No | When true, returns the full text content of each result page. | | `max_characters` | integer | No | Maximum characters of page text to return per result when include\_text is true. Defaults to 3000. | | `num_results` | integer | No | Number of similar results to return (1–100). Defaults to 10. | | `start_published_date` | string | No | Only return pages published after this date. ISO 8601 format: YYYY-MM-DDTHH:MM:SS.000Z | | `url` | string | Yes | The URL to find similar pages for. | ## `exa_get_webset` [Section titled “exa\_get\_webset”](#exa_get_webset) Get the status and details of an existing Exa Webset by its ID. Use this to poll the status of an async webset created with Create Webset. Returns metadata including status (created, running, completed, cancelled), progress, and configuration. | Name | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------- | | `webset_id` | string | Yes | The ID of the webset to retrieve. | ## `exa_list_webset_items` [Section titled “exa\_list\_webset\_items”](#exa_list_webset_items) List the collected URLs and items from a completed Exa Webset. Use this after polling Get Webset until its status is ‘completed’ to retrieve the discovered results. | Name | Type | Required | Description | | ----------- | ------- | -------- | --------------------------------------------------------------------------- | | `count` | integer | No | Number of items to return per page. Defaults to 10. | | `cursor` | string | No | Pagination cursor from a previous response to fetch the next page of items. | | `webset_id` | string | Yes | The ID of the webset to retrieve items from. | ## `exa_list_websets` [Section titled “exa\_list\_websets”](#exa_list_websets) List all Exa Websets in your account with optional pagination. Returns a list of websets with their IDs, statuses, and configurations. | Name | Type | Required | Description | | -------- | ------- | -------- | ------------------------------------------------------------------ | | `count` | integer | No | Number of websets to return per page. Defaults to 10. | | `cursor` | string | No | Pagination cursor from a previous response to fetch the next page. | ## `exa_research` [Section titled “exa\_research”](#exa_research) Run in-depth research on a topic using Exa’s neural search. Performs a semantic search and returns results with full page text and AI-generated summaries, providing structured multi-source research output. Best for comprehensive topic analysis. Rate limit: 60 requests/minute. | Name | Type | Required | Description | | ---------------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------ | | `category` | string | No | Restrict research to a specific content category for more targeted results. | | `exclude_domains` | `array` | No | JSON array of domains to exclude from research results. | | `include_domains` | `array` | No | JSON array of domains to restrict research sources to. Useful to focus on authoritative sources. | | `max_characters` | integer | No | Maximum characters of text to extract per source page. Defaults to 5000. | | `num_results` | integer | No | Number of sources to gather for the research (1–20). More sources provide broader coverage. | | `query` | string | Yes | The research topic or question to investigate across the web. | | `start_published_date` | string | No | Only include sources published after this date. ISO 8601 format. | | `summary_query` | string | No | Optional focused question to guide the AI page summaries. Defaults to the main research query. | ## `exa_search` [Section titled “exa\_search”](#exa_search) Search the web using Exa’s AI-powered semantic or keyword search engine. Supports filtering by domain, date range, content category, and result type. Optionally returns page text, highlights, or summaries alongside search results. Rate limit: 60 requests/minute. | Name | Type | Required | Description | | ---------------------- | --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `category` | string | No | Restrict results to a specific content category. | | `end_published_date` | string | No | Only return pages published before this date. ISO 8601 format: YYYY-MM-DDTHH:MM:SS.000Z | | `exclude_domains` | `array` | No | JSON array of domains to exclude from results. Example: \[“reddit.com”,“quora.com”] | | `include_domains` | `array` | No | JSON array of domains to restrict results to. Example: \[“techcrunch.com”,“wired.com”] | | `include_text` | boolean | No | When true, returns the full text content of each result page (up to max\_characters). | | `max_characters` | integer | No | Maximum characters of page text to return per result when include\_text is true. Defaults to 3000. | | `num_results` | integer | No | Number of results to return (1–100). Defaults to 10. | | `query` | string | Yes | The search query. For neural/auto type, natural language works best. For keyword type, use specific terms. | | `start_published_date` | string | No | Only return pages published after this date. ISO 8601 format: YYYY-MM-DDTHH:MM:SS.000Z | | `type` | string | No | Search type: ‘neural’ for semantic AI search (best for natural language), ‘keyword’ for exact-match keyword search, ‘auto’ to let Exa decide. | | `use_autoprompt` | boolean | No | When true, Exa automatically rewrites the query to be more semantically effective. | ## `exa_websets` [Section titled “exa\_websets”](#exa_websets) Execute a complex web query designed to discover and return large sets of URLs (up to thousands) matching specific criteria. Websets are ideal for lead generation, market research, competitor analysis, and large-scale data collection. Returns a webset ID — poll status with GET `/websets/v0/websets/{id}`. High credit consumption. | Name | Type | Required | Description | | ----------------- | --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `count` | integer | No | Target number of URLs to collect. Can range from hundreds to thousands. Higher counts take longer and consume more credits. | | `entity_type` | string | No | The type of entity to search for. Helps Exa understand what constitutes a valid result match. | | `exclude_domains` | `array` | No | JSON array of domains to exclude from webset results. | | `external_id` | string | No | Optional external identifier to tag this webset for reference in your system. | | `include_domains` | `array` | No | JSON array of domains to restrict webset sources to. | | `query` | string | Yes | The search query describing what kinds of pages or entities to find. Be specific and descriptive for best results. | --- # DOCUMENT BOUNDARY --- # Fathom > Connect to Fathom AI meeting assistant. Record, transcribe, and summarize meetings with AI-powered insights Connect to Fathom AI meeting assistant. Record, transcribe, and summarize meetings with AI-powered insights ![Fathom logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/fathom.svg) Supports authentication: API Key ## Usage [Section titled “Usage”](#usage) Connect a user’s Fathom account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Fathom in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'fathom'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Fathom:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1/users/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "fathom" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Fathom:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1/users/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Figma > Connect to Figma API v1. Read and write files, manage components, styles, variables, webhooks, and dev resources. Enterprise features include library analytics and activity logs. Connect to Figma API v1. Read and write files, manage components, styles, variables, webhooks, and dev resources. Enterprise features include library analytics and activity logs. ![Figma logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/figma.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Figma app credentials with Scalekit so it can manage the OAuth 2.0 authentication flow and token lifecycle on your behalf. You’ll need a Client ID and Client Secret from the [Figma Developers portal](https://www.figma.com/developers). 1. ### Create a Figma connection in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Search for **Figma** and click **Create**. ![Search for Figma and create a new connection](/.netlify/images?url=_astro%2Fscalekit-search-figma.DMWuHuit.png\&w=3024\&h=1622\&dpl=69cce21a4f77360008b1503a) * In the **Configure Figma Connection** panel, copy the **Redirect URI**. It looks like `https:///sso/v1/oauth//callback`. You’ll paste this into Figma in the next step. ![Copy the Redirect URI from the Configure Figma Connection panel](/.netlify/images?url=_astro%2Fconfigure-figma-connection.BNKrArhW.png\&w=1532\&h=1624\&dpl=69cce21a4f77360008b1503a) 2. ### Create an app in the Figma Developers portal * Go to the [Figma Developers portal](https://www.figma.com/developers/apps) and sign in. Click **+ Create a new app**. ![Figma Developers portal showing the My apps list and Create a new app button](/.netlify/images?url=_astro%2Ffigma-create-app.DKSqDDHd.png\&w=1200\&h=680\&dpl=69cce21a4f77360008b1503a) * Fill in your app name and description, then click **Save**. 3. ### Add the redirect URI and copy credentials * Open your app and click the **OAuth credentials** tab. * Under **Redirect URLs**, click **Add a redirect URL** and paste the Redirect URI you copied from Scalekit. * Copy the **Client ID** from the same tab. * Copy the **Client Secret**. Store it securely — never commit it to source control. ![Figma app OAuth credentials tab showing Client ID, Client Secret, and Redirect URLs](/.netlify/images?url=_astro%2Ffigma-oauth-credentials.RbfaNhD9.png\&w=1200\&h=680\&dpl=69cce21a4f77360008b1503a) Client secret is shown once The Client Secret is masked after the initial creation. If you lose it, you must generate a new one in the Figma app settings. 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the Figma connection you created. * Enter your credentials: * **Client ID** — from the Figma OAuth credentials tab * **Client Secret** — copied in the previous step * **Scopes** — select the permissions your app needs: * `files:read` — read files, nodes, images, components, and styles * `file_variables:read` — read local and published variables * `file_variables:write` — create, update, and delete variables * `webhooks:write` — create, update, and delete team webhooks ![Scalekit Figma connection with Client ID, Client Secret, and scopes filled in](/.netlify/images?url=_astro%2Ffigma-credentials-filled.VVF_XfTK.png\&w=1534\&h=1618\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Figma account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Figma in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'figma'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Step 1: Generate an authorization link and present it to your user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('Authorize Figma:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Step 2: Make API requests via the Scalekit proxy — no token management needed 25 const me = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1/me', 29 method: 'GET', 30 }); 31 console.log('Authenticated user:', me); 32 33 // Example: fetch a file's document tree 34 const file = await actions.request({ 35 connectionName, 36 identifier, 37 path: '/v1/files/YOUR_FILE_KEY', 38 method: 'GET', 39 }); 40 console.log('File:', file); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "figma" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Step 1: Generate an authorization link and present it to your user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 print("Authorize Figma:", link_response.link) 22 input("Press Enter after authorizing...") 23 24 # Step 2: Make API requests via the Scalekit proxy — no token management needed 25 me = actions.request( 26 connection_name=connection_name, 27 identifier=identifier, 28 path="/v1/me", 29 method="GET" 30 ) 31 print("Authenticated user:", me) 32 33 # Example: fetch a file's document tree 34 file = actions.request( 35 connection_name=connection_name, 36 identifier=identifier, 37 path="/v1/files/YOUR_FILE_KEY", 38 method="GET" 39 ) 40 print("File:", file) ``` ## Scalekit tools ## `figma_me_get` [Section titled “figma\_me\_get”](#figma_me_get) Returns the authenticated user’s profile information including name, email, handle, and profile image URL. No parameters required. ## `figma_file_get` [Section titled “figma\_file\_get”](#figma_file_get) Returns a Figma file’s full document tree including all nodes, components, styles, and metadata. | Name | Type | Required | Description | | ------------- | ------- | -------- | ------------------------------------------------------------------------ | | `file_key` | string | Yes | The unique key identifying the Figma file (found in the file URL) | | `version` | string | No | A specific version ID to retrieve; omit for the latest version | | `ids` | string | No | Comma-separated list of node IDs to limit the response to specific nodes | | `depth` | integer | No | Maximum depth of the document tree to return | | `geometry` | string | No | Set to `paths` to export vector path data for nodes | | `plugin_data` | string | No | Comma-separated list of plugin IDs to include plugin-specific data | | `branch_data` | boolean | No | Whether to include branch metadata in the response | ## `figma_file_nodes_get` [Section titled “figma\_file\_nodes\_get”](#figma_file_nodes_get) Returns specific nodes from a Figma file by their node IDs, along with their children and associated styles and components. | Name | Type | Required | Description | | ------------- | ------- | -------- | ----------------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `ids` | string | Yes | Comma-separated list of node IDs to fetch | | `version` | string | No | A specific version ID to retrieve | | `depth` | integer | No | Maximum depth of the subtree to return | | `geometry` | string | No | Set to `paths` to export vector path data | | `plugin_data` | string | No | Comma-separated list of plugin IDs for plugin-specific data | ## `figma_file_images_render` [Section titled “figma\_file\_images\_render”](#figma_file_images_render) Renders nodes from a Figma file as images (PNG, JPG, SVG, or PDF) and returns download URLs. | Name | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `ids` | string | Yes | Comma-separated list of node IDs to render | | `scale` | number | No | Image scale factor between 0.01 and 4 (default: 1) | | `format` | string | No | Output format: `png`, `jpg`, `svg`, or `pdf` (default: `png`) | | `svg_include_id` | boolean | No | Whether to include node IDs as attributes in SVG output | | `svg_simplify_stroke` | boolean | No | Whether to simplify inside/outside strokes in SVG output | | `use_absolute_bounds` | boolean | No | Whether to use the node’s absolute bounding box for cropping | | `version` | string | No | A specific version ID to render | ## `figma_file_image_fills_get` [Section titled “figma\_file\_image\_fills\_get”](#figma_file_image_fills_get) Returns download URLs for all image fills used in a Figma file. Image fills are images applied as fills to nodes. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | ## `figma_file_versions_list` [Section titled “figma\_file\_versions\_list”](#figma_file_versions_list) Returns the version history of a Figma file, including version IDs, labels, descriptions, and creation timestamps. | Name | Type | Required | Description | | ----------- | ------- | -------- | ----------------------------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `page_size` | integer | No | Number of versions to return per page | | `before` | integer | No | Cursor for backward pagination; returns versions before this version ID | | `after` | integer | No | Cursor for forward pagination; returns versions after this version ID | ## `figma_file_comments_list` [Section titled “figma\_file\_comments\_list”](#figma_file_comments_list) Returns all comments on a Figma file, including their text, author, position, and resolved status. | Name | Type | Required | Description | | ---------- | ------- | -------- | ---------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `as_md` | boolean | No | Whether to return comment text formatted as Markdown | ## `figma_file_comment_create` [Section titled “figma\_file\_comment\_create”](#figma_file_comment_create) Posts a new comment on a Figma file. Can be placed at a specific canvas position or anchored to a specific node. | Name | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `message` | string | Yes | The text content of the comment | | `client_meta` | object | No | Coordinates or node anchor for placing the comment on the canvas | | `comment_id` | string | No | ID of the parent comment to reply to an existing thread | ## `figma_file_comment_delete` [Section titled “figma\_file\_comment\_delete”](#figma_file_comment_delete) Deletes a specific comment from a Figma file. Only the comment author or file owner can delete a comment. | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `comment_id` | string | Yes | The ID of the comment to delete | ## `figma_comment_reactions_list` [Section titled “figma\_comment\_reactions\_list”](#figma_comment_reactions_list) Returns a list of emoji reactions on a specific comment in a Figma file. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `comment_id` | string | Yes | The ID of the comment to list reactions for | | `cursor` | string | No | Pagination cursor for fetching the next page of results | ## `figma_comment_reaction_create` [Section titled “figma\_comment\_reaction\_create”](#figma_comment_reaction_create) Adds an emoji reaction to a comment in a Figma file. | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `comment_id` | string | Yes | The ID of the comment to react to | | `emoji` | string | Yes | The emoji to add as a reaction (e.g., `:heart:`, `:+1:`) | ## `figma_comment_reaction_delete` [Section titled “figma\_comment\_reaction\_delete”](#figma_comment_reaction_delete) Removes the authenticated user’s emoji reaction from a comment in a Figma file. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------ | | `file_key` | string | Yes | The unique key identifying the Figma file | | `comment_id` | string | Yes | The ID of the comment to remove the reaction from | | `emoji` | string | Yes | The emoji reaction to remove (e.g., `:heart:`, `:+1:`) | ## `figma_file_components_list` [Section titled “figma\_file\_components\_list”](#figma_file_components_list) Returns a list of all published components in a Figma file, including their keys, names, descriptions, and thumbnails. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | ## `figma_component_get` [Section titled “figma\_component\_get”](#figma_component_get) Returns metadata for a published component by its key, including name, description, thumbnail, and containing file information. | Name | Type | Required | Description | | ----- | ------ | -------- | ---------------------------------------------------------------------- | | `key` | string | Yes | The key of the published component (from `figma_file_components_list`) | ## `figma_file_component_sets_list` [Section titled “figma\_file\_component\_sets\_list”](#figma_file_component_sets_list) Returns all published component sets in a Figma file. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | ## `figma_component_set_get` [Section titled “figma\_component\_set\_get”](#figma_component_set_get) Returns metadata for a published component set (a group of related component variants) by its key. | Name | Type | Required | Description | | ----- | ------ | -------- | -------------------------------------- | | `key` | string | Yes | The key of the published component set | ## `figma_team_components_list` [Section titled “figma\_team\_components\_list”](#figma_team_components_list) Returns all published components in a Figma team library, with pagination support. | Name | Type | Required | Description | | ----------- | ------- | -------- | --------------------------------------- | | `team_id` | string | Yes | The ID of the Figma team | | `page_size` | integer | No | Number of components to return per page | | `after` | integer | No | Cursor for forward pagination | | `before` | integer | No | Cursor for backward pagination | ## `figma_team_component_sets_list` [Section titled “figma\_team\_component\_sets\_list”](#figma_team_component_sets_list) Returns all published component sets in a Figma team library, with pagination support. | Name | Type | Required | Description | | ----------- | ------- | -------- | ------------------------------------------- | | `team_id` | string | Yes | The ID of the Figma team | | `page_size` | integer | No | Number of component sets to return per page | | `after` | integer | No | Cursor for forward pagination | | `before` | integer | No | Cursor for backward pagination | ## `figma_file_styles_list` [Section titled “figma\_file\_styles\_list”](#figma_file_styles_list) Returns all published styles in a Figma file, including color, text, effect, and grid styles. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | ## `figma_style_get` [Section titled “figma\_style\_get”](#figma_style_get) Returns metadata for a published style by its key, including name, description, style type, and containing file information. | Name | Type | Required | Description | | ----- | ------ | -------- | ------------------------------ | | `key` | string | Yes | The key of the published style | ## `figma_team_styles_list` [Section titled “figma\_team\_styles\_list”](#figma_team_styles_list) Returns all published styles in a Figma team library, with pagination support. | Name | Type | Required | Description | | ----------- | ------- | -------- | ----------------------------------- | | `team_id` | string | Yes | The ID of the Figma team | | `page_size` | integer | No | Number of styles to return per page | | `after` | integer | No | Cursor for forward pagination | | `before` | integer | No | Cursor for backward pagination | ## `figma_file_variables_local_get` [Section titled “figma\_file\_variables\_local\_get”](#figma_file_variables_local_get) Returns all local variables and variable collections defined in a Figma file. Requires the `file_variables:read` scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | ## `figma_file_variables_published_get` [Section titled “figma\_file\_variables\_published\_get”](#figma_file_variables_published_get) Returns all published variables and variable collections from a Figma file’s library. Requires the `file_variables:read` scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | ## `figma_file_variables_update` [Section titled “figma\_file\_variables\_update”](#figma_file_variables_update) Creates, updates, or deletes variables and variable collections in a Figma file. Accepts a JSON payload describing the changes. Requires the `file_variables:write` scope. | Name | Type | Required | Description | | --------------------- | ------ | -------- | ------------------------------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `variableCollections` | array | No | Variable collection changes: create, update, or delete collections | | `variableModes` | array | No | Variable mode changes: create, update, or delete modes within collections | | `variables` | array | No | Variable changes: create, update, or delete individual variables | ## `figma_team_get` [Section titled “figma\_team\_get”](#figma_team_get) Returns metadata about a Figma team, including its name and member count. | Name | Type | Required | Description | | --------- | ------ | -------- | ------------------------ | | `team_id` | string | Yes | The ID of the Figma team | ## `figma_team_projects_list` [Section titled “figma\_team\_projects\_list”](#figma_team_projects_list) Returns all projects within a Figma team that the authenticated user has access to. | Name | Type | Required | Description | | --------- | ------ | -------- | ------------------------ | | `team_id` | string | Yes | The ID of the Figma team | ## `figma_project_files_list` [Section titled “figma\_project\_files\_list”](#figma_project_files_list) Returns all files in a Figma project, including file keys, names, thumbnails, and last modified timestamps. | Name | Type | Required | Description | | ------------- | ------- | -------- | ------------------------------------------------ | | `project_id` | string | Yes | The ID of the Figma project | | `branch_data` | boolean | No | Whether to include branch metadata for each file | ## `figma_dev_resources_list` [Section titled “figma\_dev\_resources\_list”](#figma_dev_resources_list) Returns dev resources (links to external tools like Storybook, Jira, etc.) attached to nodes in a Figma file. | Name | Type | Required | Description | | ---------- | ------ | -------- | -------------------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `node_id` | string | No | Filter results to dev resources attached to a specific node ID | ## `figma_dev_resource_create` [Section titled “figma\_dev\_resource\_create”](#figma_dev_resource_create) Creates a dev resource (external link) attached to a node in a Figma file, such as a link to Storybook, Jira, or documentation. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------------------ | | `file_key` | string | Yes | The unique key identifying the Figma file | | `node_id` | string | Yes | The ID of the node to attach the dev resource to | | `name` | string | Yes | Display name for the dev resource link | | `url` | string | Yes | The URL of the external resource | ## `figma_dev_resource_update` [Section titled “figma\_dev\_resource\_update”](#figma_dev_resource_update) Updates an existing dev resource attached to a node in a Figma file. | Name | Type | Required | Description | | ----------------- | ------ | -------- | ----------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `dev_resource_id` | string | Yes | The ID of the dev resource to update | | `name` | string | No | Updated display name for the dev resource | | `url` | string | No | Updated URL for the dev resource | ## `figma_dev_resource_delete` [Section titled “figma\_dev\_resource\_delete”](#figma_dev_resource_delete) Permanently deletes a dev resource from a node in a Figma file. | Name | Type | Required | Description | | ----------------- | ------ | -------- | ----------------------------------------- | | `file_key` | string | Yes | The unique key identifying the Figma file | | `dev_resource_id` | string | Yes | The ID of the dev resource to delete | ## `figma_webhook_create` [Section titled “figma\_webhook\_create”](#figma_webhook_create) Creates a new webhook that sends events to the specified endpoint URL when Figma events occur in a team. Requires the `webhooks:write` scope. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `team_id` | string | Yes | The ID of the Figma team to register the webhook for | | `event_type` | string | Yes | The event type that triggers the webhook (e.g., `FILE_UPDATE`, `FILE_VERSION_UPDATE`, `FILE_DELETE`, `LIBRARY_PUBLISH`, `FILE_COMMENT`) | | `endpoint` | string | Yes | The HTTPS URL that receives webhook POST requests | | `passcode` | string | Yes | A secret string included in each webhook payload for request verification | | `status` | string | No | Webhook status: `ACTIVE` or `PAUSED` (default: `ACTIVE`) | | `description` | string | No | A human-readable description of the webhook’s purpose | ## `figma_webhook_get` [Section titled “figma\_webhook\_get”](#figma_webhook_get) Returns details of a specific Figma webhook by its ID, including event type, endpoint, and status. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------- | | `webhook_id` | string | Yes | The ID of the webhook to retrieve | ## `figma_webhook_update` [Section titled “figma\_webhook\_update”](#figma_webhook_update) Updates an existing Figma webhook’s endpoint, passcode, status, or description. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------------- | | `webhook_id` | string | Yes | The ID of the webhook to update | | `endpoint` | string | No | Updated HTTPS URL to receive webhook events | | `passcode` | string | No | Updated secret for verifying webhook payloads | | `status` | string | No | Updated status: `ACTIVE` or `PAUSED` | | `description` | string | No | Updated description for the webhook | ## `figma_webhook_delete` [Section titled “figma\_webhook\_delete”](#figma_webhook_delete) Permanently deletes a Figma webhook. This stops all future event deliveries for this webhook. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------- | | `webhook_id` | string | Yes | The ID of the webhook to delete | ## `figma_team_webhooks_list` [Section titled “figma\_team\_webhooks\_list”](#figma_team_webhooks_list) Returns all webhooks registered for a Figma team. | Name | Type | Required | Description | | --------- | ------ | -------- | ------------------------ | | `team_id` | string | Yes | The ID of the Figma team | ## `figma_webhook_requests_list` [Section titled “figma\_webhook\_requests\_list”](#figma_webhook_requests_list) Returns the delivery history for a webhook, including request payloads, response codes, and timestamps. | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------- | | `webhook_id` | string | Yes | The ID of the webhook to fetch request history for | ## `figma_payments_get` [Section titled “figma\_payments\_get”](#figma_payments_get) Returns payment and plan information for a Figma user or resource, including subscription status and plan type. | Name | Type | Required | Description | | ------------------- | ------ | -------- | ----------------------------------------------------------- | | `user_id` | string | No | The ID of the user to check payment status for | | `community_file_id` | string | No | The ID of a community file to check resource payment status | | `plugin_id` | string | No | The ID of a plugin to check payment status for | ## `figma_activity_logs_list` [Section titled “figma\_activity\_logs\_list”](#figma_activity_logs_list) Returns activity log events for an organization. Includes events for file edits, permissions changes, and user actions. Enterprise only Activity logs are available on Figma Enterprise plans only. The authenticated user must have organization admin permissions. | Name | Type | Required | Description | | ------------ | ------- | -------- | -------------------------------------------------------- | | `org_id` | string | Yes | The ID of the Figma organization | | `events` | string | No | Comma-separated list of event types to filter by | | `start_time` | integer | No | Unix timestamp (seconds) for the start of the time range | | `end_time` | integer | No | Unix timestamp (seconds) for the end of the time range | | `limit` | integer | No | Number of events to return per page | | `next_page` | string | No | Pagination cursor returned by the previous response | ## `figma_library_analytics_component_actions_get` [Section titled “figma\_library\_analytics\_component\_actions\_get”](#figma_library_analytics_component_actions_get) Returns analytics data on component insertion, detachment, and usage actions from a library file. Enterprise only Library analytics require a Figma Enterprise plan and the `library_analytics:read` scope. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------ | | `file_key` | string | Yes | The unique key identifying the library file | | `group_by` | string | Yes | Dimension to group results by: `component` or `team` | | `start_date` | string | No | Start date for the analytics range (ISO 8601 format, e.g., `2024-01-01`) | | `end_date` | string | No | End date for the analytics range (ISO 8601 format) | | `order` | string | No | Sort order for results: `asc` or `desc` | | `cursor` | string | No | Pagination cursor for the next page of results | ## `figma_library_analytics_component_usages_get` [Section titled “figma\_library\_analytics\_component\_usages\_get”](#figma_library_analytics_component_usages_get) Returns a snapshot of how many times each component from a library is used across the organization. Enterprise only Library analytics require a Figma Enterprise plan and the `library_analytics:read` scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the library file | | `group_by` | string | Yes | Dimension to group results by: `component` or `team` | | `cursor` | string | No | Pagination cursor for the next page of results | ## `figma_library_analytics_style_actions_get` [Section titled “figma\_library\_analytics\_style\_actions\_get”](#figma_library_analytics_style_actions_get) Returns analytics data on style insertion and detachment actions from a library file. Enterprise only Library analytics require a Figma Enterprise plan and the `library_analytics:read` scope. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the library file | | `group_by` | string | Yes | Dimension to group results by: `style` or `team` | | `start_date` | string | No | Start date for the analytics range (ISO 8601 format) | | `end_date` | string | No | End date for the analytics range (ISO 8601 format) | | `order` | string | No | Sort order for results: `asc` or `desc` | | `cursor` | string | No | Pagination cursor for the next page of results | ## `figma_library_analytics_style_usages_get` [Section titled “figma\_library\_analytics\_style\_usages\_get”](#figma_library_analytics_style_usages_get) Returns a snapshot of how many times each style from a library is used across the organization. Enterprise only Library analytics require a Figma Enterprise plan and the `library_analytics:read` scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------------------ | | `file_key` | string | Yes | The unique key identifying the library file | | `group_by` | string | Yes | Dimension to group results by: `style` or `team` | | `cursor` | string | No | Pagination cursor for the next page of results | ## `figma_library_analytics_variable_actions_get` [Section titled “figma\_library\_analytics\_variable\_actions\_get”](#figma_library_analytics_variable_actions_get) Returns analytics data on variable actions from a library file. Enterprise only Library analytics require a Figma Enterprise plan and the `library_analytics:read` scope. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the library file | | `group_by` | string | Yes | Dimension to group results by: `variable` or `team` | | `start_date` | string | No | Start date for the analytics range (ISO 8601 format) | | `end_date` | string | No | End date for the analytics range (ISO 8601 format) | | `order` | string | No | Sort order for results: `asc` or `desc` | | `cursor` | string | No | Pagination cursor for the next page of results | ## `figma_library_analytics_variable_usages_get` [Section titled “figma\_library\_analytics\_variable\_usages\_get”](#figma_library_analytics_variable_usages_get) Returns a snapshot of how many times each variable from a library is used across the organization. Enterprise only Library analytics require a Figma Enterprise plan and the `library_analytics:read` scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | --------------------------------------------------- | | `file_key` | string | Yes | The unique key identifying the library file | | `group_by` | string | Yes | Dimension to group results by: `variable` or `team` | | `cursor` | string | No | Pagination cursor for the next page of results | --- # DOCUMENT BOUNDARY --- # Freshdesk > Connect to Freshdesk. Manage tickets, contacts, companies, and customer support workflows Connect to Freshdesk. Manage tickets, contacts, companies, and customer support workflows ![Freshdesk logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/freshdesk.png) Supports authentication: Basic Auth ## Usage [Section titled “Usage”](#usage) Connect a user’s Freshdesk account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. **Don’t worry about your Freshdesk domain in the path.** Scalekit automatically resolves `{{domain}}` from the connected account’s configuration and constructs the full URL for you. For example, if your Freshdesk domain is `mycompany.freshdesk.com`, a request with `path="/v2/agents/me"` will be sent to `https://mycompany.freshdesk.com/api/v2/agents/me` automatically. You can interact with Freshdesk in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'freshdesk'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Freshdesk:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v2/agents/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "freshdesk" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Freshdesk:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v2/agents/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `freshdesk_agent_create` [Section titled “freshdesk\_agent\_create”](#freshdesk_agent_create) Create a new agent in Freshdesk. Email is required and must be unique. Agent will receive invitation email to set up account. At least one role must be assigned. | Name | Type | Required | Description | | -------------- | --------------- | -------- | ------------------------------------------------------------------------------ | | `agent_type` | number | No | Type of agent (1=Support Agent, 2=Field Agent, 3=Collaborator) | | `email` | string | Yes | Email address of the agent (must be unique) | | `focus_mode` | boolean | No | Focus mode setting for the agent | | `group_ids` | `array` | No | Array of group IDs to assign the agent to | | `language` | string | No | Language preference of the agent | | `name` | string | No | Full name of the agent | | `occasional` | boolean | No | Whether the agent is occasional (true) or full-time (false) | | `role_ids` | `array` | Yes | Array of role IDs to assign to the agent (at least one required) | | `signature` | string | No | Agent email signature in HTML format | | `skill_ids` | `array` | No | Array of skill IDs to assign to the agent | | `ticket_scope` | number | Yes | Ticket permission level (1=Global Access, 2=Group Access, 3=Restricted Access) | | `time_zone` | string | No | Time zone of the agent | ## `freshdesk_agent_delete` [Section titled “freshdesk\_agent\_delete”](#freshdesk_agent_delete) Delete an agent from Freshdesk. This action is irreversible and will remove the agent from the system. The agent will no longer have access to the helpdesk and all associated data will be permanently deleted. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------- | | `agent_id` | number | Yes | ID of the agent to delete | ## `freshdesk_agents_list` [Section titled “freshdesk\_agents\_list”](#freshdesk_agents_list) Retrieve a list of agents from Freshdesk with filtering options. Returns agent details including IDs, contact information, roles, and availability status. Supports pagination with up to 100 agents per page. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------------- | | `email` | string | No | Filter agents by email address | | `mobile` | string | No | Filter agents by mobile number | | `page` | number | No | Page number for pagination (starts from 1) | | `per_page` | number | No | Number of agents per page (max 100) | | `phone` | string | No | Filter agents by phone number | | `state` | string | No | Filter agents by state (fulltime or occasional) | ## `freshdesk_contact_create` [Section titled “freshdesk\_contact\_create”](#freshdesk_contact_create) Create a new contact in Freshdesk. Email and name are required. Supports custom fields, company assignment, and contact segmentation. | Name | Type | Required | Description | | --------------- | --------------- | -------- | ------------------------------------------- | | `address` | string | No | Address of the contact | | `company_id` | number | No | Company ID to associate with the contact | | `custom_fields` | `object` | No | Key-value pairs for custom field values | | `description` | string | No | Description about the contact | | `email` | string | Yes | Email address of the contact | | `job_title` | string | No | Job title of the contact | | `language` | string | No | Language preference of the contact | | `mobile` | string | No | Mobile number of the contact | | `name` | string | Yes | Full name of the contact | | `phone` | string | No | Phone number of the contact | | `tags` | `array` | No | Array of tags to associate with the contact | | `time_zone` | string | No | Time zone of the contact | ## `freshdesk_roles_list` [Section titled “freshdesk\_roles\_list”](#freshdesk_roles_list) Retrieve a list of all roles from Freshdesk. Returns role details including IDs, names, descriptions, default status, and timestamps. This endpoint provides information about the different permission levels and access controls available in the Freshdesk system. ## `freshdesk_ticket_create` [Section titled “freshdesk\_ticket\_create”](#freshdesk_ticket_create) Create a new ticket in Freshdesk. Requires either requester\_id, email, facebook\_id, phone, twitter\_id, or unique\_external\_id to identify the requester. | Name | Type | Required | Description | | --------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------ | | `cc_emails` | `array` | No | Array of email addresses to be added in CC | | `custom_fields` | `object` | No | Key-value pairs containing custom field names and values | | `description` | string | No | HTML content of the ticket describing the issue | | `email` | string | No | Email address of the requester. If no contact exists, will be added as new contact. | | `group_id` | number | No | ID of the group to which the ticket has been assigned | | `name` | string | No | Name of the requester | | `priority` | number | No | Priority of the ticket. 1=Low, 2=Medium, 3=High, 4=Urgent | | `requester_id` | number | No | User ID of the requester. For existing contacts, can be passed instead of email. | | `responder_id` | number | No | ID of the agent to whom the ticket has been assigned | | `source` | number | No | Channel through which ticket was created. 1=Email, 2=Portal, 3=Phone, 7=Chat, 9=Feedback Widget, 10=Outbound Email | | `status` | number | No | Status of the ticket. 2=Open, 3=Pending, 4=Resolved, 5=Closed | | `subject` | string | No | Subject of the ticket | | `tags` | `array` | No | Array of tags to be associated with the ticket | | `type` | string | No | Helps categorize the ticket according to different kinds of issues | ## `freshdesk_ticket_get` [Section titled “freshdesk\_ticket\_get”](#freshdesk_ticket_get) Retrieve details of a specific ticket by ID. Includes ticket properties, conversations, and metadata. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------------------------------------- | | `include` | string | No | Additional resources to include (stats, requester, company, conversations) | | `ticket_id` | number | Yes | ID of the ticket to retrieve | ## `freshdesk_ticket_update` [Section titled “freshdesk\_ticket\_update”](#freshdesk_ticket_update) Update an existing ticket in Freshdesk. Note: Subject and description of outbound tickets cannot be updated. | Name | Type | Required | Description | | --------------- | --------------- | -------- | ------------------------------------------------------------------- | | `custom_fields` | `object` | No | Key-value pairs containing custom field names and values | | `description` | string | No | HTML content of the ticket (cannot be updated for outbound tickets) | | `group_id` | number | No | ID of the group to which the ticket has been assigned | | `name` | string | No | Name of the requester | | `priority` | number | No | Priority of the ticket. 1=Low, 2=Medium, 3=High, 4=Urgent | | `responder_id` | number | No | ID of the agent to whom the ticket has been assigned | | `status` | number | No | Status of the ticket. 2=Open, 3=Pending, 4=Resolved, 5=Closed | | `subject` | string | No | Subject of the ticket (cannot be updated for outbound tickets) | | `tags` | `array` | No | Array of tags to be associated with the ticket | | `ticket_id` | number | Yes | ID of the ticket to update | ## `freshdesk_tickets_list` [Section titled “freshdesk\_tickets\_list”](#freshdesk_tickets_list) Retrieve a list of tickets with filtering and pagination. Supports filtering by status, priority, requester, and more. Returns 30 tickets per page by default. | Name | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------ | | `company_id` | number | No | Filter by company ID | | `email` | string | No | Filter by requester email | | `filter` | string | No | Filter name (new\_and\_my\_open, watching, spam, deleted) | | `include` | string | No | Additional resources to include (description, requester, company, stats) | | `page` | number | No | Page number for pagination (starts from 1) | | `per_page` | number | No | Number of tickets per page (max 100) | | `requester_id` | number | No | Filter by requester ID | | `updated_since` | string | No | Filter tickets updated since this timestamp (ISO 8601) | ## `freshdesk_tickets_reply` [Section titled “freshdesk\_tickets\_reply”](#freshdesk_tickets_reply) Add a public reply to a ticket conversation. The reply will be visible to the customer and will update the ticket status if specified. | Name | Type | Required | Description | | ------------ | --------------- | -------- | -------------------------------------------- | | `bcc_emails` | `array` | No | Array of email addresses to BCC on the reply | | `body` | string | Yes | HTML content of the reply | | `cc_emails` | `array` | No | Array of email addresses to CC on the reply | | `from_email` | string | No | Email address to send the reply from | | `ticket_id` | number | Yes | ID of the ticket to reply to | | `user_id` | number | No | ID of the agent sending the reply | --- # DOCUMENT BOUNDARY --- # Github > GitHub is a cloud-based Git repository hosting service that allows developers to store, manage, and track changes to their code. GitHub is a cloud-based Git repository hosting service that allows developers to store, manage, and track changes to their code. ![Github logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/github.png) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the GitHub connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **GitHub** and click **Create**. Note By default, a connection using Scalekit’s credentials will be created. If you are testing, go directly to the Usage section. Before going to production, update your connection by following the steps below. * Click **Use your own credentials** and copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.2UesZwzd.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Go to [GitHub Developer Settings](https://github.com/settings/developers) and open your OAuth app. * Under **General**, paste the copied URI into the **Authorization callback URL** field and click **Save application**. ![Add callback URL in GitHub OAuth app settings](/.netlify/images?url=_astro%2Fadd-redirect-uri.DmNiWjPG.gif\&w=1168\&h=912\&dpl=69cce21a4f77360008b1503a) 2. ### Get client credentials In [GitHub Developer Settings](https://github.com/settings/developers), open your OAuth app: * **Client ID** — listed on the app’s main settings page * **Client Secret** — click **Generate a new client secret** if you don’t have one 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from your GitHub OAuth app) * Client Secret (from your GitHub OAuth app) ![Add credentials for GitHub in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s GitHub account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with GitHub in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'github'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize GitHub:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/user', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "github" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize GitHub:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/user", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `github_file_contents_get` [Section titled “github\_file\_contents\_get”](#github_file_contents_get) Get the contents of a file or directory from a GitHub repository. Returns Base64 encoded content for files. | Name | Type | Required | Description | | ------- | ------ | -------- | ----------------------------------------------------------- | | `owner` | string | Yes | The account owner of the repository | | `path` | string | Yes | The content path (file or directory path in the repository) | | `ref` | string | No | The name of the commit/branch/tag | | `repo` | string | Yes | The name of the repository | ## `github_file_create_update` [Section titled “github\_file\_create\_update”](#github_file_create_update) Create a new file or update an existing file in a GitHub repository. Content must be Base64 encoded. Requires SHA when updating existing files. | Name | Type | Required | Description | | ----------- | -------- | -------- | ------------------------------------------------------------------------------- | | `author` | `object` | No | Author information object with name and email | | `branch` | string | No | The branch name | | `committer` | `object` | No | Committer information object with name and email | | `content` | string | Yes | The new file content (Base64 encoded) | | `message` | string | Yes | The commit message for this change | | `owner` | string | Yes | The account owner of the repository | | `path` | string | Yes | The file path in the repository | | `repo` | string | Yes | The name of the repository | | `sha` | string | No | The blob SHA of the file being replaced (required when updating existing files) | ## `github_issue_create` [Section titled “github\_issue\_create”](#github_issue_create) Create a new issue in a repository. Requires push access to set assignees, milestones, and labels. | Name | Type | Required | Description | | ----------- | --------------- | -------- | -------------------------------------------- | | `assignees` | `array` | No | GitHub usernames to assign to the issue | | `body` | string | No | The contents of the issue | | `labels` | `array` | No | Labels to associate with the issue | | `milestone` | number | No | Milestone number to associate with the issue | | `owner` | string | Yes | The account owner of the repository | | `repo` | string | Yes | The name of the repository | | `title` | string | Yes | The title of the issue | | `type` | string | No | The name of the issue type | ## `github_issues_list` [Section titled “github\_issues\_list”](#github_issues_list) List issues in a repository. Both issues and pull requests are returned as issues in the GitHub API. | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------------------------------------- | | `assignee` | string | No | Filter by assigned user | | `creator` | string | No | Filter by issue creator | | `direction` | string | No | Sort order | | `labels` | string | No | Filter by comma-separated list of label names | | `milestone` | string | No | Filter by milestone number or state | | `owner` | string | Yes | The account owner of the repository | | `page` | number | No | Page number of results to fetch | | `per_page` | number | No | Number of results per page (max 100) | | `repo` | string | Yes | The name of the repository | | `since` | string | No | Show issues updated after this timestamp (ISO 8601 format) | | `sort` | string | No | Property to sort issues by | | `state` | string | No | Filter by issue state | ## `github_public_repos_list` [Section titled “github\_public\_repos\_list”](#github_public_repos_list) List public repositories for a specified user. Does not require authentication. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------- | | `direction` | string | No | Sort order | | `page` | number | No | Page number of results to fetch | | `per_page` | number | No | Number of results per page (max 100) | | `sort` | string | No | Property to sort repositories by | | `type` | string | No | Filter repositories by type | | `username` | string | Yes | The GitHub username to list repositories for | ## `github_pull_request_create` [Section titled “github\_pull\_request\_create”](#github_pull_request_create) Create a new pull request in a repository. Requires write access to the head branch. | Name | Type | Required | Description | | ----------------------- | ------- | -------- | ------------------------------------------------------------------------------- | | `base` | string | Yes | The name of the branch you want the changes pulled into | | `body` | string | No | The contents of the pull request description | | `draft` | boolean | No | Indicates whether the pull request is a draft | | `head` | string | Yes | The name of the branch where your changes are implemented (format: user:branch) | | `maintainer_can_modify` | boolean | No | Indicates whether maintainers can modify the pull request | | `owner` | string | Yes | The account owner of the repository | | `repo` | string | Yes | The name of the repository | | `title` | string | No | The title of the pull request | ## `github_pull_requests_list` [Section titled “github\_pull\_requests\_list”](#github_pull_requests_list) List pull requests in a repository with optional filtering by state, head, and base branches. | Name | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------- | | `base` | string | No | Filter by base branch name | | `direction` | string | No | Sort order | | `head` | string | No | Filter by head branch (format: user:ref-name) | | `owner` | string | Yes | The account owner of the repository | | `page` | number | No | Page number of results to fetch | | `per_page` | number | No | Number of results per page (max 100) | | `repo` | string | Yes | The name of the repository | | `sort` | string | No | Property to sort pull requests by | | `state` | string | No | Filter by pull request state | ## `github_repo_get` [Section titled “github\_repo\_get”](#github_repo_get) Get detailed information about a GitHub repository including metadata, settings, and statistics. | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------------------------------------------------------ | | `owner` | string | Yes | The account owner of the repository (case-insensitive) | | `repo` | string | Yes | The name of the repository without the .git extension (case-insensitive) | ## `github_user_repos_list` [Section titled “github\_user\_repos\_list”](#github_user_repos_list) List repositories for the authenticated user. Requires authentication. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------ | | `direction` | string | No | Sort order | | `page` | number | No | Page number of results to fetch | | `per_page` | number | No | Number of results per page (max 100) | | `sort` | string | No | Property to sort repositories by | | `type` | string | No | Filter repositories by type | --- # DOCUMENT BOUNDARY --- # GitLab > Connect to GitLab to manage repositories, issues, merge requests, CI/CD pipelines, groups, releases, and more with 110 tools. Connect to GitLab to manage repositories, issues, merge requests, CI/CD pipelines, groups, and releases — all from your agent. ![GitLab logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/gitlab.svg) Supports authentication: OAuth 2.0 What you can build with this connector | Use case | Tools involved | | ----------------------------- | ----------------------------------------------------------------------------------------------------- | | **Automated code review bot** | `gitlab_merge_requests_list` → `gitlab_merge_request_diff_get` → `gitlab_merge_request_note_create` | | **CI/CD health monitor** | `gitlab_pipelines_list` → `gitlab_pipeline_get` → `gitlab_pipeline_retry` (on failure) | | **Issue triage agent** | `gitlab_issues_list` → `gitlab_issue_update` (add labels/milestone) → `gitlab_issue_note_create` | | **Release automation** | `gitlab_commits_list` → `gitlab_tag_create` → `gitlab_release_create` | | **Repository scaffolding** | `gitlab_project_create` → `gitlab_branch_create` → `gitlab_file_create` → `gitlab_project_member_add` | | **Security audit** | `gitlab_project_variables_list` → `gitlab_deploy_keys_list` → `gitlab_project_webhooks_list` | | **Onboarding automation** | `gitlab_group_member_add` → `gitlab_project_member_add` → `gitlab_issue_create` (onboarding task) | | **Dependency update bot** | `gitlab_file_get` → `gitlab_branch_create` → `gitlab_file_update` → `gitlab_merge_request_create` | **Key concepts:** * **Project ID vs. path**: Most tools accept a numeric `project_id` or a URL-encoded path like `namespace%2Fproject`. Paths are easier to read; IDs are stable even after renames. * **IID vs. ID**: Issues and merge requests have both a global `id` and an internal `iid` (per-project sequential number). The `iid` is what users see in the UI (e.g., `#42`). All tools that take `issue_iid` or `merge_request_iid` expect the `iid`. * **Access levels**: GitLab uses numeric access levels — `10` = Guest, `20` = Reporter, `30` = Developer, `40` = Maintainer, `50` = Owner. Pass these as integers to member management tools. * **Pipeline identity verification**: On GitLab.com, triggering pipelines via API requires the authenticated user to have completed identity verification at `gitlab.com/-/profile/verify`. * **Premium-only tools**: `gitlab_merge_request_approve` and `gitlab_merge_request_approvals_get` require **GitLab Premium** or higher. On Free plans these endpoints return `403 Forbidden`. * **Pagination**: All list endpoints support `page` (1-based) and `per_page` (default `20`, max `100`). Use `x-next-page` from the response header to paginate. * **Self-managed**: Replace `https://gitlab.com` with your instance URL in all `path` arguments when using a self-managed GitLab instance. ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the GitLab connector so Scalekit handles the OAuth 2.0 flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **GitLab** and click **Create**. Note By default, a connection using Scalekit’s credentials will be created. If you are testing, go directly to the Usage section. Before going to production, update your connection by following the steps below. * Click **Use your own credentials** and copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.BOmi_1g6.png\&w=1456\&h=816\&dpl=69cce21a4f77360008b1503a) * Go to [GitLab Applications settings](https://gitlab.com/-/profile/applications) (**User Settings** → **Applications**) and open or create your OAuth application. * Paste the copied URI into the **Redirect URI** field and click **Save application**. ![Add redirect URI and scopes in GitLab OAuth app](/.netlify/images?url=_astro%2Fcreate-oauth-app.fa9GUpVm.png\&w=1168\&h=860\&dpl=69cce21a4f77360008b1503a) * Under **Scopes**, select the permissions your agent needs: | Scope | Access granted | Use when | | ------------------ | ------------------------------------------- | ------------------------------------------------------------------ | | `api` | Full read/write access to all API endpoints | Most tools — recommended for full access | | `read_user` | Current user’s profile | `gitlab_current_user_get` only | | `read_api` | Read-only access to all API endpoints | Read-only agents | | `read_repository` | Read access to repositories | File and commit reads only | | `write_repository` | Push access to repositories | `gitlab_file_create`, `gitlab_file_update`, `gitlab_branch_create` | Use api scope for full access The `api` scope grants complete REST and GraphQL access and covers all 110 tools in this connector. Use `read_api` alone if your agent only reads data. GitLab SaaS vs. self-managed These steps are for **GitLab.com** (SaaS). If your team uses a self-managed GitLab instance, replace `gitlab.com` with your instance hostname in all URLs. 2. ### Get client credentials After saving the application, GitLab shows the **Application ID** and **Secret** on the application detail page: ![GitLab application detail page with Application ID and Secret](/.netlify/images?url=_astro%2Fget-credentials.CcNeu2sF.png\&w=1168\&h=520\&dpl=69cce21a4f77360008b1503a) * **Application ID** — listed on the app’s main settings page * **Secret** — shown only once after creation; if you lose it, regenerate it from the same page 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * **Client ID** — paste your GitLab Application ID * **Client Secret** — paste your GitLab Secret ![Add GitLab credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.B6vMZpv-.png\&w=1168\&h=680\&dpl=69cce21a4f77360008b1503a) * Click **Save**. Connection name is your identifier The connection name you set here (e.g., `gitlab`) is the string you pass to `connection_name` (Python) or `connectionName` (Node.js) in every SDK call. It must match exactly — including case. ## Usage [Section titled “Usage”](#usage) Connect a user’s GitLab account and make API calls on their behalf — Scalekit handles OAuth and token refresh automatically. * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'gitlab'; // connection name from Scalekit dashboard 5 const identifier = 'user_123'; // your unique user identifier 6 7 const scalekit = new ScalekitClient( 8 process.env.SCALEKIT_ENV_URL, 9 process.env.SCALEKIT_CLIENT_ID, 10 process.env.SCALEKIT_CLIENT_SECRET 11 ); 12 const actions = scalekit.actions; 13 14 // Step 1: Get the authorization link and send it to your user 15 const { link } = await actions.getAuthorizationLink({ connectionName, identifier }); 16 console.log('🔗 Authorize GitLab:', link); 17 18 // Step 2: After the user authorizes, make API calls via Scalekit proxy 19 // Example: list open issues for a project 20 const issues = await actions.request({ 21 connectionName, 22 identifier, 23 path: '/api/v4/projects/my-group%2Fmy-repo/issues', 24 method: 'GET', 25 params: { state: 'opened', per_page: 50 }, 26 }); 27 console.log(issues.data); // Array of issue objects ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "gitlab" # connection name from Scalekit dashboard 6 identifier = "user_123" # your unique user identifier 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_ENV_URL"), 12 ) 13 actions = scalekit_client.actions 14 15 # Step 1: Get the authorization link and send it to your user 16 link_response = actions.get_authorization_link( 17 connection_name=connection_name, 18 identifier=identifier 19 ) 20 print("🔗 Authorize GitLab:", link_response.link) 21 22 # Step 2: After the user authorizes, make API calls via Scalekit proxy 23 # Example: list open issues for a project 24 issues = actions.request( 25 connection_name=connection_name, 26 identifier=identifier, 27 path="/api/v4/projects/my-group%2Fmy-repo/issues", 28 method="GET", 29 params={"state": "opened", "per_page": 50}, 30 ) 31 print(issues["data"]) # List of issue objects ``` GitLab API base URL The GitLab REST API base URL is `https://gitlab.com`. Scalekit prefixes it automatically — pass only the path starting with `/api/v4/`. For self-managed instances, set your instance URL as the base in your Scalekit connection settings. ![Scalekit Connected Accounts tab showing authorized GitLab users with status, token expiry, and date added](/.netlify/images?url=_astro%2Fadd-connected-account.B6q-T-il.png\&w=1280\&h=560\&dpl=69cce21a4f77360008b1503a) ### Scalekit tools [Section titled “Scalekit tools”](#scalekit-tools) Use `actions.execute_tool()` (Python) or `actions.executeTool()` (Node.js) to call any GitLab tool by name. Scalekit resolves credentials, calls the GitLab API, and returns a structured response. **Create a merge request and request review:** * Node.js ```typescript 1 // Create a merge request from a feature branch 2 const mr = await actions.executeTool({ 3 connectionName: 'gitlab', 4 identifier: 'user_123', 5 toolName: 'gitlab_merge_request_create', 6 toolInput: { 7 project_id: 'my-group/my-repo', 8 source_branch: 'feature/add-auth', 9 target_branch: 'main', 10 title: 'feat: add OAuth 2.0 authentication', 11 description: '## Summary\n\nAdds GitLab OAuth integration.\n\n/assign @reviewer', 12 remove_source_branch: true, 13 squash: false, 14 }, 15 }); 16 const mrIid = mr.data.iid; 17 console.log(`Created MR !${mrIid}: ${mr.data.web_url}`); 18 19 // Add a comment to kick off review 20 await actions.executeTool({ 21 connectionName: 'gitlab', 22 identifier: 'user_123', 23 toolName: 'gitlab_merge_request_note_create', 24 toolInput: { 25 project_id: 'my-group/my-repo', 26 merge_request_iid: mrIid, 27 body: '🤖 This MR was created automatically. @reviewer please review when you get a chance.', 28 }, 29 }); ``` * Python ```python 1 # Create a merge request from a feature branch 2 mr = actions.execute_tool( 3 connection_name="gitlab", 4 identifier="user_123", 5 tool_name="gitlab_merge_request_create", 6 tool_input={ 7 "project_id": "my-group/my-repo", 8 "source_branch": "feature/add-auth", 9 "target_branch": "main", 10 "title": "feat: add OAuth 2.0 authentication", 11 "description": "## Summary\n\nAdds GitLab OAuth integration.\n\n/assign @reviewer", 12 "remove_source_branch": True, 13 "squash": False, 14 }, 15 ) 16 mr_iid = mr["data"]["iid"] 17 print(f"Created MR !{mr_iid}: {mr['data']['web_url']}") 18 19 # Add a comment to kick off review 20 actions.execute_tool( 21 connection_name="gitlab", 22 identifier="user_123", 23 tool_name="gitlab_merge_request_note_create", 24 tool_input={ 25 "project_id": "my-group/my-repo", 26 "merge_request_iid": mr_iid, 27 "body": "🤖 This MR was created automatically. @reviewer please review when you get a chance.", 28 }, 29 ) ``` **Trigger a pipeline and wait for result:** * Node.js ```typescript 1 // Trigger a pipeline on the main branch 2 // Note: requires identity verification at gitlab.com/-/profile/verify 3 const pipeline = await actions.executeTool({ 4 connectionName: 'gitlab', 5 identifier: 'user_123', 6 toolName: 'gitlab_pipeline_create', 7 toolInput: { 8 project_id: 'my-group/my-repo', 9 ref: 'main', 10 }, 11 }); 12 console.log(`Pipeline #${pipeline.data.id} triggered — status: ${pipeline.data.status}`); 13 14 // Fetch pipeline jobs to check individual job status 15 const jobs = await actions.executeTool({ 16 connectionName: 'gitlab', 17 identifier: 'user_123', 18 toolName: 'gitlab_pipeline_jobs_list', 19 toolInput: { 20 project_id: 'my-group/my-repo', 21 pipeline_id: pipeline.data.id, 22 }, 23 }); 24 for (const job of jobs.data) { 25 console.log(` [${job.status}] ${job.name} — stage: ${job.stage}`); 26 } ``` * Python ```python 1 # Trigger a pipeline on the main branch 2 # Note: requires identity verification at gitlab.com/-/profile/verify 3 pipeline = actions.execute_tool( 4 connection_name="gitlab", 5 identifier="user_123", 6 tool_name="gitlab_pipeline_create", 7 tool_input={"project_id": "my-group/my-repo", "ref": "main"}, 8 ) 9 print(f"Pipeline #{pipeline['data']['id']} triggered — status: {pipeline['data']['status']}") 10 11 # Fetch pipeline jobs to check individual job status 12 jobs = actions.execute_tool( 13 connection_name="gitlab", 14 identifier="user_123", 15 tool_name="gitlab_pipeline_jobs_list", 16 tool_input={ 17 "project_id": "my-group/my-repo", 18 "pipeline_id": pipeline["data"]["id"], 19 }, 20 ) 21 for job in jobs["data"]: 22 print(f" [{job['status']}] {job['name']} — stage: {job['stage']}") ``` **Create a file in a new branch and open a merge request:** * Node.js ```typescript 1 const project = 'my-group/my-repo'; 2 const branch = `bot/add-config-${Date.now()}`; 3 4 // Create a new branch off main 5 await actions.executeTool({ 6 connectionName: 'gitlab', 7 identifier: 'user_123', 8 toolName: 'gitlab_branch_create', 9 toolInput: { project_id: project, branch, ref: 'main' }, 10 }); 11 12 // Add a new config file 13 await actions.executeTool({ 14 connectionName: 'gitlab', 15 identifier: 'user_123', 16 toolName: 'gitlab_file_create', 17 toolInput: { 18 project_id: project, 19 file_path: 'config/feature-flags.json', 20 branch, 21 content: JSON.stringify({ newFeature: false }, null, 2), 22 commit_message: 'chore: add feature flag config', 23 }, 24 }); 25 26 // Open a merge request 27 const mr = await actions.executeTool({ 28 connectionName: 'gitlab', 29 identifier: 'user_123', 30 toolName: 'gitlab_merge_request_create', 31 toolInput: { 32 project_id: project, 33 source_branch: branch, 34 target_branch: 'main', 35 title: 'chore: add feature flag config', 36 remove_source_branch: true, 37 }, 38 }); 39 console.log(`MR !${mr.data.iid} opened: ${mr.data.web_url}`); ``` * Python ```python 1 import time 2 3 project = "my-group/my-repo" 4 branch = f"bot/add-config-{int(time.time())}" 5 6 # Create a new branch off main 7 actions.execute_tool( 8 connection_name="gitlab", 9 identifier="user_123", 10 tool_name="gitlab_branch_create", 11 tool_input={"project_id": project, "branch": branch, "ref": "main"}, 12 ) 13 14 # Add a new config file 15 import json 16 actions.execute_tool( 17 connection_name="gitlab", 18 identifier="user_123", 19 tool_name="gitlab_file_create", 20 tool_input={ 21 "project_id": project, 22 "file_path": "config/feature-flags.json", 23 "branch": branch, 24 "content": json.dumps({"newFeature": False}, indent=2), 25 "commit_message": "chore: add feature flag config", 26 }, 27 ) 28 29 # Open a merge request 30 mr = actions.execute_tool( 31 connection_name="gitlab", 32 identifier="user_123", 33 tool_name="gitlab_merge_request_create", 34 tool_input={ 35 "project_id": project, 36 "source_branch": branch, 37 "target_branch": "main", 38 "title": "chore: add feature flag config", 39 "remove_source_branch": True, 40 }, 41 ) 42 print(f"MR !{mr['data']['iid']} opened: {mr['data']['web_url']}") ``` **Triage stale open issues:** * Node.js ```typescript 1 // List all open issues updated more than 90 days ago 2 const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(); 3 4 const issues = await actions.executeTool({ 5 connectionName: 'gitlab', 6 identifier: 'user_123', 7 toolName: 'gitlab_issues_list', 8 toolInput: { 9 project_id: 'my-group/my-repo', 10 state: 'opened', 11 updated_before: cutoff, 12 per_page: 100, 13 }, 14 }); 15 16 for (const issue of issues.data) { 17 // Add a stale label and comment 18 await actions.executeTool({ 19 connectionName: 'gitlab', 20 identifier: 'user_123', 21 toolName: 'gitlab_issue_update', 22 toolInput: { 23 project_id: 'my-group/my-repo', 24 issue_iid: issue.iid, 25 add_labels: 'stale', 26 }, 27 }); 28 await actions.executeTool({ 29 connectionName: 'gitlab', 30 identifier: 'user_123', 31 toolName: 'gitlab_issue_note_create', 32 toolInput: { 33 project_id: 'my-group/my-repo', 34 issue_iid: issue.iid, 35 body: '🤖 This issue has had no activity in 90 days and has been marked as stale. It will be closed in 14 days if no further activity occurs.', 36 }, 37 }); 38 console.log(`Marked issue #${issue.iid} as stale`); 39 } ``` * Python ```python 1 from datetime import datetime, timedelta, timezone 2 3 # List all open issues updated more than 90 days ago 4 cutoff = (datetime.now(timezone.utc) - timedelta(days=90)).isoformat() 5 6 issues = actions.execute_tool( 7 connection_name="gitlab", 8 identifier="user_123", 9 tool_name="gitlab_issues_list", 10 tool_input={ 11 "project_id": "my-group/my-repo", 12 "state": "opened", 13 "updated_before": cutoff, 14 "per_page": 100, 15 }, 16 ) 17 18 for issue in issues["data"]: 19 # Add a stale label and comment 20 actions.execute_tool( 21 connection_name="gitlab", 22 identifier="user_123", 23 tool_name="gitlab_issue_update", 24 tool_input={ 25 "project_id": "my-group/my-repo", 26 "issue_iid": issue["iid"], 27 "add_labels": "stale", 28 }, 29 ) 30 actions.execute_tool( 31 connection_name="gitlab", 32 identifier="user_123", 33 tool_name="gitlab_issue_note_create", 34 tool_input={ 35 "project_id": "my-group/my-repo", 36 "issue_iid": issue["iid"], 37 "body": "🤖 This issue has had no activity in 90 days and has been marked as stale. It will be closed in 14 days if no further activity occurs.", 38 }, 39 ) 40 print(f"Marked issue #{issue['iid']} as stale") ``` ## Tool list [Section titled “Tool list”](#tool-list) ### Projects [Section titled “Projects”](#projects) ## `gitlab_projects_list` [Section titled “gitlab\_projects\_list”](#gitlab_projects_list) List all projects accessible to the authenticated user. Supports filtering by search term, ownership, membership, and visibility. | Name | Type | Required | Description | | ------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------- | | `search` | string | No | Filter projects by name | | `owned` | boolean | No | Return only projects owned by the current user | | `membership` | boolean | No | Return only projects the user is a member of | | `starred` | boolean | No | Return only starred projects | | `visibility` | string | No | Filter by visibility: `public`, `internal`, `private` | | `order_by` | string | No | Sort by: `id`, `name`, `path`, `created_at`, `updated_at`, `last_activity_at`. Defaults to `created_at` | | `sort` | string | No | Sort direction: `asc` or `desc` | | `page` | number | No | Page number (1-based) | | `per_page` | number | No | Results per page. Max `100`, defaults to `20` | ## `gitlab_project_get` [Section titled “gitlab\_project\_get”](#gitlab_project_get) Get a specific project by numeric ID or URL-encoded namespace/project path (e.g., `my-group%2Fmy-repo`). | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------------------------------- | | `project_id` | string | Yes | Numeric project ID or URL-encoded path (e.g., `42` or `my-group%2Fmy-repo`) | ## `gitlab_project_create` [Section titled “gitlab\_project\_create”](#gitlab_project_create) Create a new GitLab project under the authenticated user’s namespace or a specified group. | Name | Type | Required | Description | | ------------------------ | ------- | -------- | ------------------------------------------------------------------------------------- | | `name` | string | Yes | Project name | | `path` | string | No | Project path (URL slug). Defaults to a slugified version of `name` | | `namespace_id` | number | No | Numeric ID of the group namespace. Omit to create under the user’s personal namespace | | `description` | string | No | Project description | | `visibility` | string | No | `public`, `internal`, or `private`. Defaults to `private` | | `initialize_with_readme` | boolean | No | Initialize with a `README.md` file | | `default_branch` | string | No | Default branch name. Defaults to `main` | ## `gitlab_project_update` [Section titled “gitlab\_project\_update”](#gitlab_project_update) Update an existing GitLab project’s settings. | Name | Type | Required | Description | | --------------------------------------- | ------- | -------- | ----------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `name` | string | No | New project name | | `description` | string | No | New description | | `visibility` | string | No | New visibility: `public`, `internal`, `private` | | `default_branch` | string | No | New default branch name | | `topics` | array | No | Array of topic strings (replaces all existing topics) | | `merge_method` | string | No | `merge`, `rebase_merge`, or `ff` (fast-forward only) | | `only_allow_merge_if_pipeline_succeeds` | boolean | No | Block merges unless the pipeline passes | | `remove_source_branch_after_merge` | boolean | No | Automatically delete source branch after merge | ## `gitlab_project_delete` [Section titled “gitlab\_project\_delete”](#gitlab_project_delete) Delete a GitLab project. This is an asynchronous operation — the API returns `202 Accepted` immediately and deletion proceeds in the background. Requires **Owner** role. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | ## `gitlab_project_fork` [Section titled “gitlab\_project\_fork”](#gitlab_project_fork) Fork a GitLab project into a specified namespace. Returns the new forked project object. | Name | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------------------------------------------------------- | | `project_id` | string | Yes | ID or path of the project to fork | | `namespace_id` | number | No | Numeric namespace ID to fork into. Defaults to the user’s personal namespace | | `name` | string | No | Name for the forked project | | `path` | string | No | Path for the forked project | ## `gitlab_project_star` [Section titled “gitlab\_project\_star”](#gitlab_project_star) Star a GitLab project. Returns the project object. Returns `304 Not Modified` if already starred. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | ## `gitlab_project_unstar` [Section titled “gitlab\_project\_unstar”](#gitlab_project_unstar) Unstar a GitLab project. Returns `304 Not Modified` if the project was not starred. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | ## `gitlab_project_search` [Section titled “gitlab\_project\_search”](#gitlab_project_search) Search within a specific GitLab project for issues, merge requests, commits, code, blobs, and more. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------ | | `project_id` | string | Yes | Project ID or path | | `scope` | string | Yes | What to search: `issues`, `merge_requests`, `milestones`, `notes`, `wiki_blobs`, `commits`, `blobs`, `users` | | `search` | string | Yes | Search query | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_project_forks_list` [Section titled “gitlab\_project\_forks\_list”](#gitlab_project_forks_list) List all forks of a GitLab project. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------- | | `project_id` | string | Yes | Project ID or path | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_namespaces_list` [Section titled “gitlab\_namespaces\_list”](#gitlab_namespaces_list) List all namespaces accessible to the current user — personal namespaces and groups. Useful for resolving where to create a project or fork. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------- | | `search` | string | No | Filter namespaces by name | | `page` | number | No | Page number | | `per_page` | number | No | Results per page | ## `gitlab_global_search` [Section titled “gitlab\_global\_search”](#gitlab_global_search) Search globally across GitLab for projects, issues, merge requests, commits, blobs, and more. | Name | Type | Required | Description | | ---------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | `scope` | string | Yes | What to search: `projects`, `issues`, `merge_requests`, `milestones`, `snippet_titles`, `wiki_blobs`, `commits`, `blobs`, `users` | | `search` | string | Yes | Search query (minimum 2 characters) | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ### Repository — Branches [Section titled “Repository — Branches”](#repository--branches) ## `gitlab_branches_list` [Section titled “gitlab\_branches\_list”](#gitlab_branches_list) List all branches in a GitLab project repository. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------- | | `project_id` | string | Yes | Project ID or path | | `search` | string | No | Filter branches by name | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_branch_get` [Section titled “gitlab\_branch\_get”](#gitlab_branch_get) Get details of a specific branch, including the latest commit on that branch. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `branch` | string | Yes | Branch name | ## `gitlab_branch_create` [Section titled “gitlab\_branch\_create”](#gitlab_branch_create) Create a new branch in a GitLab repository from a specified commit, branch, or tag. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `branch` | string | Yes | Name of the new branch | | `ref` | string | Yes | Source branch name, tag name, or commit SHA to branch from | ## `gitlab_branch_delete` [Section titled “gitlab\_branch\_delete”](#gitlab_branch_delete) Delete a branch from a GitLab repository. Protected branches cannot be deleted unless unprotected first. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------- | | `project_id` | string | Yes | Project ID or path | | `branch` | string | Yes | Branch name to delete | ### Repository — Tags [Section titled “Repository — Tags”](#repository--tags) ## `gitlab_tags_list` [Section titled “gitlab\_tags\_list”](#gitlab_tags_list) List all tags in a GitLab project repository. | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `search` | string | No | Filter tags by name | | `order_by` | string | No | Sort by `name`, `version`, or `updated`. Defaults to `updated` | | `sort` | string | No | `asc` or `desc` | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_tag_get` [Section titled “gitlab\_tag\_get”](#gitlab_tag_get) Get details of a specific repository tag, including the commit it points to and any associated release notes. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `tag_name` | string | Yes | Tag name | ## `gitlab_tag_create` [Section titled “gitlab\_tag\_create”](#gitlab_tag_create) Create a new tag in a GitLab repository. Optionally include a message to create an annotated tag. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `tag_name` | string | Yes | Name of the tag to create | | `ref` | string | Yes | Branch, tag, or commit SHA to tag | | `message` | string | No | Tag message. If provided, an annotated tag is created instead of a lightweight tag | ## `gitlab_tag_delete` [Section titled “gitlab\_tag\_delete”](#gitlab_tag_delete) Delete a tag from a GitLab repository. Protected tags cannot be deleted. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------- | | `project_id` | string | Yes | Project ID or path | | `tag_name` | string | Yes | Name of the tag to delete | ### Repository — Commits [Section titled “Repository — Commits”](#repository--commits) ## `gitlab_commits_list` [Section titled “gitlab\_commits\_list”](#gitlab_commits_list) List commits for a GitLab project, optionally filtered by branch, path, or date range. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `ref_name` | string | No | Branch, tag, or commit SHA to list commits from. Defaults to the default branch | | `since` | string | No | ISO 8601 datetime — only commits after this date | | `until` | string | No | ISO 8601 datetime — only commits before this date | | `path` | string | No | Filter commits by file path | | `author` | string | No | Filter by commit author name or email | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_commit_get` [Section titled “gitlab\_commit\_get”](#gitlab_commit_get) Get detailed information about a specific commit by its SHA, including stats and diff summary. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------ | | `project_id` | string | Yes | Project ID or path | | `sha` | string | Yes | Full or short commit SHA | ## `gitlab_commit_diff_get` [Section titled “gitlab\_commit\_diff\_get”](#gitlab_commit_diff_get) Get the full diff of a specific commit — lists all changed files with their hunks. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `sha` | string | Yes | Commit SHA | | `page` | number | No | Page number (diffs are paginated for large commits) | | `per_page` | number | No | Results per page | ## `gitlab_commit_comment_create` [Section titled “gitlab\_commit\_comment\_create”](#gitlab_commit_comment_create) Add an inline or general comment to a specific commit. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------ | | `project_id` | string | Yes | Project ID or path | | `sha` | string | Yes | Commit SHA | | `note` | string | Yes | Comment text | | `path` | string | No | File path for inline comment | | `line` | number | No | Line number for inline comment | | `line_type` | string | No | `new` or `old` — which side of the diff the line is on | ## `gitlab_commit_comments_list` [Section titled “gitlab\_commit\_comments\_list”](#gitlab_commit_comments_list) List all comments on a specific commit. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `sha` | string | Yes | Commit SHA | | `page` | number | No | Page number | | `per_page` | number | No | Results per page | ## `gitlab_compare_refs` [Section titled “gitlab\_compare\_refs”](#gitlab_compare_refs) Compare two refs (branches, tags, or commit SHAs) and return the commits and diff between them. | Name | Type | Required | Description | | ------------ | ------- | -------- | --------------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `from` | string | Yes | Source ref (branch name, tag, or commit SHA) | | `to` | string | Yes | Target ref to compare against | | `straight` | boolean | No | If `true`, computes the diff directly between `from` and `to` instead of using the merge base | ### Repository — Files & Trees [Section titled “Repository — Files & Trees”](#repository--files--trees) ## `gitlab_file_get` [Section titled “gitlab\_file\_get”](#gitlab_file_get) Get a file’s raw content and metadata (size, encoding, last commit) from a GitLab repository at a specific ref. | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `file_path` | string | Yes | URL-encoded file path within the repository (e.g., `src%2Findex.ts`) | | `ref` | string | Yes | Branch, tag, or commit SHA to read the file from | ## `gitlab_file_create` [Section titled “gitlab\_file\_create”](#gitlab_file_create) Create a new file in a GitLab repository. The file content must be provided as a plain string (GitLab handles base64 encoding internally). | Name | Type | Required | Description | | ---------------- | ------ | -------- | -------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `file_path` | string | Yes | Path of the new file within the repository (e.g., `src/config.json`) | | `branch` | string | Yes | Branch to commit the new file to | | `content` | string | Yes | File content as a string | | `commit_message` | string | Yes | Commit message | | `author_name` | string | No | Author name override | | `author_email` | string | No | Author email override | ## `gitlab_file_update` [Section titled “gitlab\_file\_update”](#gitlab_file_update) Update the content of an existing file in a GitLab repository. The current file must exist at the specified path and branch. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `file_path` | string | Yes | Path of the file to update | | `branch` | string | Yes | Branch containing the file | | `content` | string | Yes | New file content | | `commit_message` | string | Yes | Commit message | | `last_commit_id` | string | No | The commit SHA of the last known version of the file. Used for conflict detection — GitLab rejects the update if the file has changed since this SHA | | `author_name` | string | No | Author name override | | `author_email` | string | No | Author email override | ## `gitlab_file_delete` [Section titled “gitlab\_file\_delete”](#gitlab_file_delete) Delete a file from a GitLab repository. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------ | | `project_id` | string | Yes | Project ID or path | | `file_path` | string | Yes | Path of the file to delete | | `branch` | string | Yes | Branch to delete the file from | | `commit_message` | string | Yes | Commit message | | `author_name` | string | No | Author name override | | `author_email` | string | No | Author email override | ## `gitlab_repository_tree_list` [Section titled “gitlab\_repository\_tree\_list”](#gitlab_repository_tree_list) List files and directories in a GitLab repository at a given path and ref. | Name | Type | Required | Description | | ------------ | ------- | -------- | -------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `path` | string | No | Directory path within the repository. Defaults to the root (`/`) | | `ref` | string | No | Branch, tag, or commit SHA. Defaults to the project’s default branch | | `recursive` | boolean | No | If `true`, lists files recursively across all subdirectories | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ### Issues [Section titled “Issues”](#issues) ## `gitlab_issues_list` [Section titled “gitlab\_issues\_list”](#gitlab_issues_list) List issues for a GitLab project with extensive filtering options. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `state` | string | No | Filter by state: `opened`, `closed`, or `all`. Defaults to `opened` | | `labels` | string | No | Comma-separated label names to filter by | | `milestone` | string | No | Milestone title to filter by | | `assignee_id` | number | No | Filter by assignee user ID | | `author_id` | number | No | Filter by author user ID | | `search` | string | No | Search in title and description | | `created_after` | string | No | ISO 8601 datetime — issues created after this date | | `created_before` | string | No | ISO 8601 datetime — issues created before this date | | `updated_after` | string | No | ISO 8601 datetime — issues updated after this date | | `updated_before` | string | No | ISO 8601 datetime — issues updated before this date | | `order_by` | string | No | Sort by: `created_at`, `updated_at`, `priority`, `due_date`, `relative_position`. Defaults to `created_at` | | `sort` | string | No | `asc` or `desc` | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_issue_get` [Section titled “gitlab\_issue\_get”](#gitlab_issue_get) Get a specific issue by its project-level internal ID (IID). The IID is the number shown in the GitLab UI (e.g., `#42`). | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `issue_iid` | number | Yes | Issue internal ID (IID) — the number shown in the GitLab UI | ## `gitlab_issue_create` [Section titled “gitlab\_issue\_create”](#gitlab_issue_create) Create a new issue in a GitLab project. | Name | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `title` | string | Yes | Issue title | | `description` | string | No | Issue body — supports GitLab Flavored Markdown and quick actions (e.g., `/assign @user`, `/label ~bug`) | | `assignee_ids` | array | No | Array of user IDs to assign | | `milestone_id` | number | No | ID of the milestone to link | | `labels` | string | No | Comma-separated label names to apply | | `due_date` | string | No | Due date in `YYYY-MM-DD` format | | `weight` | number | No | Issue weight (integer). **GitLab Premium+** | | `confidential` | boolean | No | Mark the issue as confidential | ## `gitlab_issue_update` [Section titled “gitlab\_issue\_update”](#gitlab_issue_update) Update an existing issue. Only fields you provide are changed. | Name | Type | Required | Description | | --------------- | ------- | -------- | ------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `issue_iid` | number | Yes | Issue IID | | `title` | string | No | New title | | `description` | string | No | New description | | `state_event` | string | No | `close` or `reopen` | | `assignee_ids` | array | No | New array of assignee user IDs (replaces existing) | | `milestone_id` | number | No | New milestone ID. Pass `0` to remove the milestone | | `labels` | string | No | Comma-separated new label list (replaces all existing labels) | | `add_labels` | string | No | Comma-separated labels to add without removing existing ones | | `remove_labels` | string | No | Comma-separated labels to remove | | `due_date` | string | No | New due date (`YYYY-MM-DD`). Pass empty string to clear | | `weight` | number | No | New weight. **GitLab Premium+** | | `confidential` | boolean | No | Update confidentiality | ## `gitlab_issue_delete` [Section titled “gitlab\_issue\_delete”](#gitlab_issue_delete) Permanently delete an issue from a GitLab project. **Requires project Owner role or admin access.** This action cannot be undone. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `issue_iid` | number | Yes | Issue IID | ### Issue Notes (Comments) [Section titled “Issue Notes (Comments)”](#issue-notes-comments) ## `gitlab_issue_notes_list` [Section titled “gitlab\_issue\_notes\_list”](#gitlab_issue_notes_list) List all comments (notes) on a specific issue. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------ | | `project_id` | string | Yes | Project ID or path | | `issue_iid` | number | Yes | Issue IID | | `sort` | string | No | `asc` or `desc`. Defaults to `asc` | | `order_by` | string | No | Sort by `created_at` or `updated_at` | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_issue_note_create` [Section titled “gitlab\_issue\_note\_create”](#gitlab_issue_note_create) Add a comment to a specific issue. Supports GitLab Flavored Markdown and quick actions. | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `issue_iid` | number | Yes | Issue IID | | `body` | string | Yes | Comment text. Supports Markdown and quick actions (e.g., `/close`, `/assign @user`) | | `created_at` | string | No | ISO 8601 datetime override for the note timestamp. **Requires admin access** | ## `gitlab_issue_note_update` [Section titled “gitlab\_issue\_note\_update”](#gitlab_issue_note_update) Update the content of an existing comment on an issue. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------- | | `project_id` | string | Yes | Project ID or path | | `issue_iid` | number | Yes | Issue IID | | `note_id` | number | Yes | Note (comment) ID | | `body` | string | Yes | New comment content | ## `gitlab_issue_note_delete` [Section titled “gitlab\_issue\_note\_delete”](#gitlab_issue_note_delete) Delete a comment from a specific issue. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `issue_iid` | number | Yes | Issue IID | | `note_id` | number | Yes | Note ID to delete | ### Merge Requests [Section titled “Merge Requests”](#merge-requests) ## `gitlab_merge_requests_list` [Section titled “gitlab\_merge\_requests\_list”](#gitlab_merge_requests_list) List merge requests for a GitLab project with filtering by state, labels, assignee, and more. | Name | Type | Required | Description | | ---------------- | ------ | -------- | --------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `state` | string | No | Filter by state: `opened`, `closed`, `merged`, `locked`, or `all`. Defaults to `opened` | | `labels` | string | No | Comma-separated label names to filter by | | `milestone` | string | No | Milestone title to filter by | | `author_id` | number | No | Filter by author user ID | | `assignee_id` | number | No | Filter by assignee user ID | | `reviewer_id` | number | No | Filter by reviewer user ID | | `source_branch` | string | No | Filter by source branch name | | `target_branch` | string | No | Filter by target branch name | | `search` | string | No | Search in title and description | | `created_after` | string | No | ISO 8601 datetime | | `created_before` | string | No | ISO 8601 datetime | | `order_by` | string | No | Sort by `created_at`, `updated_at`, or `title` | | `sort` | string | No | `asc` or `desc` | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_merge_request_get` [Section titled “gitlab\_merge\_request\_get”](#gitlab_merge_request_get) Get a specific merge request by its project-level IID. | Name | Type | Required | Description | | ------------------- | ------ | -------- | ------------------------------------------------------------------ | | `project_id` | string | Yes | Project ID or path | | `merge_request_iid` | number | Yes | Merge request IID (the number shown in the GitLab UI, e.g., `!12`) | ## `gitlab_merge_request_create` [Section titled “gitlab\_merge\_request\_create”](#gitlab_merge_request_create) Create a new merge request in a GitLab project. | Name | Type | Required | Description | | ---------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `source_branch` | string | Yes | Branch to merge from | | `target_branch` | string | Yes | Branch to merge into | | `title` | string | Yes | Merge request title | | `description` | string | No | Merge request body. Supports Markdown and quick actions (e.g., `/assign @user`, `/label ~feature`) | | `assignee_ids` | array | No | Array of user IDs to assign | | `reviewer_ids` | array | No | Array of user IDs to request review from | | `labels` | string | No | Comma-separated label names | | `milestone_id` | number | No | Milestone ID to link | | `remove_source_branch` | boolean | No | Delete the source branch after merge | | `squash` | boolean | No | Squash all commits into one when merging | | `draft` | boolean | No | Mark the merge request as a draft (cannot be merged until undrafted) | ## `gitlab_merge_request_update` [Section titled “gitlab\_merge\_request\_update”](#gitlab_merge_request_update) Update an existing merge request. Only the fields you provide are changed. | Name | Type | Required | Description | | ---------------------- | ------- | -------- | -------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `merge_request_iid` | number | Yes | Merge request IID | | `title` | string | No | New title | | `description` | string | No | New description | | `state_event` | string | No | `close` or `reopen` | | `target_branch` | string | No | New target branch | | `assignee_ids` | array | No | New assignees (replaces existing) | | `reviewer_ids` | array | No | New reviewers | | `labels` | string | No | New labels (replaces existing) | | `add_labels` | string | No | Labels to add without clearing existing ones | | `remove_labels` | string | No | Labels to remove | | `milestone_id` | number | No | New milestone ID. Pass `0` to remove | | `remove_source_branch` | boolean | No | Update the remove-source-branch preference | | `squash` | boolean | No | Update the squash preference | | `draft` | boolean | No | Mark as draft (`true`) or ready (`false`) | ## `gitlab_merge_request_merge` [Section titled “gitlab\_merge\_request\_merge”](#gitlab_merge_request_merge) Merge an open merge request. The MR must be mergeable — all required approvals satisfied, no conflicts, and pipeline passing (if required by the project settings). | Name | Type | Required | Description | | ----------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `merge_request_iid` | number | Yes | Merge request IID | | `merge_commit_message` | string | No | Custom merge commit message | | `squash_commit_message` | string | No | Custom commit message when squashing | | `should_remove_source_branch` | boolean | No | Override the remove-source-branch setting for this merge | | `squash` | boolean | No | Override the squash setting for this merge | | `sha` | string | No | If provided, the MR is merged only if the HEAD of the source branch matches this SHA — prevents merging if the branch has been updated since you last checked | ## `gitlab_merge_request_approve` [Section titled “gitlab\_merge\_request\_approve”](#gitlab_merge_request_approve) Approve a merge request. **Requires GitLab Premium or higher.** On GitLab Free, this endpoint returns `403 Forbidden`. | Name | Type | Required | Description | | ------------------- | ------ | -------- | -------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `merge_request_iid` | number | Yes | Merge request IID | | `sha` | string | No | Approve only if the MR HEAD matches this SHA | ## `gitlab_merge_request_approvals_get` [Section titled “gitlab\_merge\_request\_approvals\_get”](#gitlab_merge_request_approvals_get) Get the approval state of a merge request — who has approved and who is required to approve. **Requires GitLab Premium or higher.** | Name | Type | Required | Description | | ------------------- | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `merge_request_iid` | number | Yes | Merge request IID | ## `gitlab_merge_request_diff_get` [Section titled “gitlab\_merge\_request\_diff\_get”](#gitlab_merge_request_diff_get) Get the full diff of a merge request — all changed files and their hunks. For large MRs, results are paginated. | Name | Type | Required | Description | | ------------------- | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `merge_request_iid` | number | Yes | Merge request IID | | `page` | number | No | Page number | | `per_page` | number | No | Results per page | ## `gitlab_merge_request_commits_list` [Section titled “gitlab\_merge\_request\_commits\_list”](#gitlab_merge_request_commits_list) List all commits included in a specific merge request. | Name | Type | Required | Description | | ------------------- | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `merge_request_iid` | number | Yes | Merge request IID | ### Merge Request Notes (Comments) [Section titled “Merge Request Notes (Comments)”](#merge-request-notes-comments) ## `gitlab_merge_request_notes_list` [Section titled “gitlab\_merge\_request\_notes\_list”](#gitlab_merge_request_notes_list) List all comments on a specific merge request. | Name | Type | Required | Description | | ------------------- | ------ | -------- | ------------------------------------ | | `project_id` | string | Yes | Project ID or path | | `merge_request_iid` | number | Yes | Merge request IID | | `sort` | string | No | `asc` or `desc` | | `order_by` | string | No | Sort by `created_at` or `updated_at` | | `page` | number | No | Page number | | `per_page` | number | No | Results per page | ## `gitlab_merge_request_note_create` [Section titled “gitlab\_merge\_request\_note\_create”](#gitlab_merge_request_note_create) Add a comment to a specific merge request. Supports Markdown and quick actions. | Name | Type | Required | Description | | ------------------- | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `merge_request_iid` | number | Yes | Merge request IID | | `body` | string | Yes | Comment text | ### Pipelines & CI/CD [Section titled “Pipelines & CI/CD”](#pipelines--cicd) ## `gitlab_pipelines_list` [Section titled “gitlab\_pipelines\_list”](#gitlab_pipelines_list) List pipelines for a GitLab project with filtering by status, ref, and date. | Name | Type | Required | Description | | ---------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `status` | string | No | Filter by status: `created`, `waiting_for_resource`, `preparing`, `pending`, `running`, `success`, `failed`, `canceled`, `skipped`, `manual`, `scheduled` | | `ref` | string | No | Filter by branch or tag name | | `sha` | string | No | Filter by commit SHA | | `updated_after` | string | No | ISO 8601 datetime | | `updated_before` | string | No | ISO 8601 datetime | | `order_by` | string | No | Sort by `id`, `status`, `ref`, or `updated_at` | | `sort` | string | No | `asc` or `desc` | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_pipeline_get` [Section titled “gitlab\_pipeline\_get”](#gitlab_pipeline_get) Get detailed information about a specific pipeline, including status, duration, and triggered user. | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `pipeline_id` | number | Yes | Pipeline ID | ## `gitlab_pipeline_create` [Section titled “gitlab\_pipeline\_create”](#gitlab_pipeline_create) Trigger a new CI/CD pipeline for a specific branch or tag. **Important:** On GitLab.com, triggering pipelines via API requires the authenticated user to have completed identity verification at `https://gitlab.com/-/profile/verify`. Without verification, the API returns `403 Forbidden`. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `ref` | string | Yes | Branch name, tag name, or commit SHA to run the pipeline on | | `variables` | array | No | Array of `{ "key": "VAR_NAME", "value": "VALUE" }` objects to pass as pipeline variables | ## `gitlab_pipeline_cancel` [Section titled “gitlab\_pipeline\_cancel”](#gitlab_pipeline_cancel) Cancel a running pipeline. Only pipelines in `pending` or `running` state can be canceled. | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `pipeline_id` | number | Yes | Pipeline ID | ## `gitlab_pipeline_retry` [Section titled “gitlab\_pipeline\_retry”](#gitlab_pipeline_retry) Retry all failed jobs in a pipeline. Returns the updated pipeline object. | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `pipeline_id` | number | Yes | Pipeline ID | ## `gitlab_pipeline_delete` [Section titled “gitlab\_pipeline\_delete”](#gitlab_pipeline_delete) Delete a pipeline and its associated job traces and artifacts. This action is permanent and cannot be undone. | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `pipeline_id` | number | Yes | Pipeline ID | ## `gitlab_pipeline_jobs_list` [Section titled “gitlab\_pipeline\_jobs\_list”](#gitlab_pipeline_jobs_list) List all jobs in a specific pipeline, including their status, stage, name, and duration. | Name | Type | Required | Description | | ------------- | --------------- | -------- | ----------------------------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `pipeline_id` | number | Yes | Pipeline ID | | `scope` | string or array | No | Filter by job status: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, `manual` | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ### Jobs [Section titled “Jobs”](#jobs) ## `gitlab_jobs_list` [Section titled “gitlab\_jobs\_list”](#gitlab_jobs_list) List all jobs for a GitLab project across all pipelines. | Name | Type | Required | Description | | ------------ | --------------- | -------- | --------------------------- | | `project_id` | string | Yes | Project ID or path | | `scope` | string or array | No | Filter by job status | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_job_get` [Section titled “gitlab\_job\_get”](#gitlab_job_get) Get detailed information about a specific CI/CD job. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `job_id` | number | Yes | Job ID | ## `gitlab_job_cancel` [Section titled “gitlab\_job\_cancel”](#gitlab_job_cancel) Cancel a specific CI/CD job. The job must be in `pending` or `running` state. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `job_id` | number | Yes | Job ID | ## `gitlab_job_retry` [Section titled “gitlab\_job\_retry”](#gitlab_job_retry) Retry a specific CI/CD job. Creates a new job run with the same configuration. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `job_id` | number | Yes | Job ID | ## `gitlab_job_log_get` [Section titled “gitlab\_job\_log\_get”](#gitlab_job_log_get) Get the full trace/log output of a CI/CD job. Returns raw text. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `job_id` | number | Yes | Job ID | ## `gitlab_job_artifacts_download` [Section titled “gitlab\_job\_artifacts\_download”](#gitlab_job_artifacts_download) Download the artifacts archive of a specific CI/CD job as a binary ZIP file. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `job_id` | number | Yes | Job ID | ### CI/CD Variables [Section titled “CI/CD Variables”](#cicd-variables) ## `gitlab_project_variables_list` [Section titled “gitlab\_project\_variables\_list”](#gitlab_project_variables_list) List all CI/CD variables configured for a GitLab project. Variable values for `masked` variables are hidden (shown as `null`). | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `page` | number | No | Page number | | `per_page` | number | No | Results per page | ## `gitlab_project_variable_get` [Section titled “gitlab\_project\_variable\_get”](#gitlab_project_variable_get) Get a specific CI/CD variable by key. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `key` | string | Yes | Variable key name | ## `gitlab_project_variable_create` [Section titled “gitlab\_project\_variable\_create”](#gitlab_project_variable_create) Create a new CI/CD variable for a GitLab project. Use `masked` to prevent the value from appearing in job logs. | Name | Type | Required | Description | | ------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `key` | string | Yes | Variable key name (uppercase letters, digits, and underscores only) | | `value` | string | Yes | Variable value | | `variable_type` | string | No | `env_var` (default) or `file` — file variables are written to a temp file and the path is passed as the variable value | | `protected` | boolean | No | If `true`, the variable is only exposed on protected branches and tags | | `masked` | boolean | No | If `true`, the variable value is hidden in job logs | | `environment_scope` | string | No | Limit variable to a specific environment (e.g., `production`). Use `*` for all environments | ## `gitlab_project_variable_update` [Section titled “gitlab\_project\_variable\_update”](#gitlab_project_variable_update) Update an existing CI/CD variable. | Name | Type | Required | Description | | ------------------- | ------- | -------- | ---------------------- | | `project_id` | string | Yes | Project ID or path | | `key` | string | Yes | Variable key to update | | `value` | string | No | New value | | `variable_type` | string | No | `env_var` or `file` | | `protected` | boolean | No | New protected flag | | `masked` | boolean | No | New masked flag | | `environment_scope` | string | No | New environment scope | ## `gitlab_project_variable_delete` [Section titled “gitlab\_project\_variable\_delete”](#gitlab_project_variable_delete) Delete a CI/CD variable from a GitLab project. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------- | | `project_id` | string | Yes | Project ID or path | | `key` | string | Yes | Variable key to delete | ### Groups [Section titled “Groups”](#groups) ## `gitlab_groups_list` [Section titled “gitlab\_groups\_list”](#gitlab_groups_list) List all groups accessible to the authenticated user. | Name | Type | Required | Description | | ---------- | ------- | -------- | --------------------------------------------- | | `search` | string | No | Filter groups by name | | `owned` | boolean | No | Return only groups the user owns | | `order_by` | string | No | Sort by `name`, `path`, `id`, or `similarity` | | `sort` | string | No | `asc` or `desc` | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_group_get` [Section titled “gitlab\_group\_get”](#gitlab_group_get) Get a specific group by numeric ID or URL-encoded path. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------------------------------- | | `group_id` | string | Yes | Numeric group ID or URL-encoded path (e.g., `42` or `my-company`) | ## `gitlab_group_create` [Section titled “gitlab\_group\_create”](#gitlab_group_create) Create a new GitLab group or subgroup. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------------------------- | | `name` | string | Yes | Group name | | `path` | string | Yes | Group path (URL slug) | | `description` | string | No | Group description | | `visibility` | string | No | `public`, `internal`, or `private`. Defaults to `private` | | `parent_id` | number | No | Parent group ID. Provide to create a subgroup | ## `gitlab_group_update` [Section titled “gitlab\_group\_update”](#gitlab_group_update) Update a GitLab group’s settings. Requires Maintainer or Owner role on the group. | Name | Type | Required | Description | | ------------- | ------ | -------- | ---------------------- | | `group_id` | string | Yes | Group ID or path | | `name` | string | No | New group name | | `path` | string | No | New group path | | `description` | string | No | New description | | `visibility` | string | No | New visibility setting | ## `gitlab_group_delete` [Section titled “gitlab\_group\_delete”](#gitlab_group_delete) Delete a GitLab group and all projects within it. This is an asynchronous operation. **Requires Owner role.** This action cannot be undone. | Name | Type | Required | Description | | ---------- | ------ | -------- | ---------------- | | `group_id` | string | Yes | Group ID or path | ## `gitlab_group_projects_list` [Section titled “gitlab\_group\_projects\_list”](#gitlab_group_projects_list) List all projects belonging to a specific group. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------------------------------- | | `group_id` | string | Yes | Group ID or path | | `search` | string | No | Filter projects by name | | `visibility` | string | No | Filter by visibility | | `order_by` | string | No | Sort by `id`, `name`, `path`, `created_at`, `updated_at`, `last_activity_at` | | `sort` | string | No | `asc` or `desc` | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ### Group Members [Section titled “Group Members”](#group-members) ## `gitlab_group_members_list` [Section titled “gitlab\_group\_members\_list”](#gitlab_group_members_list) List members of a GitLab group, including their access level and expiry date. | Name | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------- | | `group_id` | string | Yes | Group ID or path | | `search` | string | No | Filter members by name or username | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_group_member_add` [Section titled “gitlab\_group\_member\_add”](#gitlab_group_member_add) Add a user to a GitLab group with a specified access level. | Name | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------------------------------------------------------------------------- | | `group_id` | string | Yes | Group ID or path | | `user_id` | number | Yes | ID of the user to add | | `access_level` | number | Yes | Access level: `10` = Guest, `20` = Reporter, `30` = Developer, `40` = Maintainer, `50` = Owner | | `expires_at` | string | No | Membership expiry date in `YYYY-MM-DD` format | ## `gitlab_group_member_remove` [Section titled “gitlab\_group\_member\_remove”](#gitlab_group_member_remove) Remove a member from a GitLab group. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------ | | `group_id` | string | Yes | Group ID or path | | `user_id` | number | Yes | ID of the user to remove | ### Project Members [Section titled “Project Members”](#project-members) ## `gitlab_project_members_list` [Section titled “gitlab\_project\_members\_list”](#gitlab_project_members_list) List members of a GitLab project, including their access level. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------- | | `project_id` | string | Yes | Project ID or path | | `search` | string | No | Filter members by name or username | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_project_member_add` [Section titled “gitlab\_project\_member\_add”](#gitlab_project_member_add) Add a user to a GitLab project with a specified access level. | Name | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `user_id` | number | Yes | ID of the user to add | | `access_level` | number | Yes | Access level: `10` = Guest, `20` = Reporter, `30` = Developer, `40` = Maintainer, `50` = Owner | | `expires_at` | string | No | Membership expiry date in `YYYY-MM-DD` format | ## `gitlab_project_member_remove` [Section titled “gitlab\_project\_member\_remove”](#gitlab_project_member_remove) Remove a member from a GitLab project. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------ | | `project_id` | string | Yes | Project ID or path | | `user_id` | number | Yes | ID of the user to remove | ### Users [Section titled “Users”](#users) ## `gitlab_current_user_get` [Section titled “gitlab\_current\_user\_get”](#gitlab_current_user_get) Get the profile of the currently authenticated user — useful for resolving the user’s ID, username, and namespace before making other calls. This tool takes no parameters. ## `gitlab_user_get` [Section titled “gitlab\_user\_get”](#gitlab_user_get) Get a specific user’s public profile by numeric user ID. | Name | Type | Required | Description | | --------- | ------ | -------- | --------------- | | `user_id` | number | Yes | Numeric user ID | ## `gitlab_users_list` [Section titled “gitlab\_users\_list”](#gitlab_users_list) List users. Supports filtering by username, search term, and active status. **Admin access** is required to list all users; otherwise only publicly visible users are returned. | Name | Type | Required | Description | | ---------- | ------- | -------- | ------------------------------------------ | | `search` | string | No | Filter by name, username, or email | | `username` | string | No | Exact username match | | `active` | boolean | No | Return only active users | | `blocked` | boolean | No | Return only blocked users (**admin only**) | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_user_projects_list` [Section titled “gitlab\_user\_projects\_list”](#gitlab_user_projects_list) List all projects owned by a specific user. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------- | | `user_id` | number | Yes | User ID | | `visibility` | string | No | Filter by visibility | | `order_by` | string | No | Sort order | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ### SSH Keys [Section titled “SSH Keys”](#ssh-keys) ## `gitlab_current_user_ssh_keys_list` [Section titled “gitlab\_current\_user\_ssh\_keys\_list”](#gitlab_current_user_ssh_keys_list) List all SSH keys for the currently authenticated user. This tool takes no parameters. ## `gitlab_ssh_key_add` [Section titled “gitlab\_ssh\_key\_add”](#gitlab_ssh_key_add) Add a new SSH public key to the currently authenticated user’s account. | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------------- | | `title` | string | Yes | Label for the key (e.g., `Work MacBook`) | | `key` | string | Yes | Full SSH public key string (e.g., `ssh-ed25519 AAAA...`) | | `expires_at` | string | No | Key expiry date in ISO 8601 format | ### Milestones [Section titled “Milestones”](#milestones) ## `gitlab_milestones_list` [Section titled “gitlab\_milestones\_list”](#gitlab_milestones_list) List milestones for a GitLab project. | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `state` | string | No | Filter by state: `active` or `closed`. Defaults to `active` | | `search` | string | No | Search milestones by title | | `page` | number | No | Page number | | `per_page` | number | No | Results per page | ## `gitlab_milestone_get` [Section titled “gitlab\_milestone\_get”](#gitlab_milestone_get) Get a specific project milestone by ID. | Name | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `milestone_id` | number | Yes | Milestone ID (not IID — use the numeric ID returned from `gitlab_milestones_list`) | ## `gitlab_milestone_create` [Section titled “gitlab\_milestone\_create”](#gitlab_milestone_create) Create a new milestone in a GitLab project. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------- | | `project_id` | string | Yes | Project ID or path | | `title` | string | Yes | Milestone title | | `description` | string | No | Milestone description | | `due_date` | string | No | Due date in `YYYY-MM-DD` format | | `start_date` | string | No | Start date in `YYYY-MM-DD` format | ## `gitlab_milestone_update` [Section titled “gitlab\_milestone\_update”](#gitlab_milestone_update) Update an existing milestone. | Name | Type | Required | Description | | -------------- | ------ | -------- | --------------------- | | `project_id` | string | Yes | Project ID or path | | `milestone_id` | number | Yes | Milestone ID | | `title` | string | No | New title | | `description` | string | No | New description | | `due_date` | string | No | New due date | | `start_date` | string | No | New start date | | `state_event` | string | No | `close` or `activate` | ## `gitlab_milestone_delete` [Section titled “gitlab\_milestone\_delete”](#gitlab_milestone_delete) Permanently delete a milestone. Issues and merge requests linked to it will lose their milestone association. | Name | Type | Required | Description | | -------------- | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `milestone_id` | number | Yes | Milestone ID | ### Labels [Section titled “Labels”](#labels) ## `gitlab_issue_labels_list` [Section titled “gitlab\_issue\_labels\_list”](#gitlab_issue_labels_list) List all labels defined in a GitLab project. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------- | | `project_id` | string | Yes | Project ID or path | | `search` | string | No | Filter labels by name | | `page` | number | No | Page number | | `per_page` | number | No | Results per page | ## `gitlab_label_create` [Section titled “gitlab\_label\_create”](#gitlab_label_create) Create a new label in a GitLab project. | Name | Type | Required | Description | | ------------- | ------ | -------- | ----------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `name` | string | Yes | Label name | | `color` | string | Yes | Label color in `#RRGGBB` hex format (e.g., `#FF5733`) | | `description` | string | No | Label description | | `priority` | number | No | Numeric priority. Used for ordering in the label list | ### Releases [Section titled “Releases”](#releases) ## `gitlab_releases_list` [Section titled “gitlab\_releases\_list”](#gitlab_releases_list) List all releases for a GitLab project. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `order_by` | string | No | Sort by `released_at` or `created_at` | | `sort` | string | No | `asc` or `desc` | | `page` | number | No | Page number | | `per_page` | number | No | Results per page. Max `100` | ## `gitlab_release_get` [Section titled “gitlab\_release\_get”](#gitlab_release_get) Get a specific release by its tag name. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `tag_name` | string | Yes | Tag name the release is associated with | ## `gitlab_release_create` [Section titled “gitlab\_release\_create”](#gitlab_release_create) Create a new release associated with an existing tag. | Name | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `tag_name` | string | Yes | Tag to associate this release with. The tag must already exist | | `name` | string | No | Release name (e.g., `v1.2.0`) | | `description` | string | No | Release notes in Markdown format | | `released_at` | string | No | ISO 8601 datetime for the release date. Defaults to now | | `assets` | object | No | Release assets object: `{ "links": [{ "name": "Binary", "url": "https://..." }] }` | ## `gitlab_release_update` [Section titled “gitlab\_release\_update”](#gitlab_release_update) Update an existing release’s name or description. | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `tag_name` | string | Yes | Tag name of the release to update | | `name` | string | No | New release name | | `description` | string | No | New release notes | | `released_at` | string | No | New release date | | `milestones` | array | No | Array of milestone titles to associate with the release | ## `gitlab_release_delete` [Section titled “gitlab\_release\_delete”](#gitlab_release_delete) Delete a release from a GitLab project. The associated tag is **not** deleted. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------- | | `project_id` | string | Yes | Project ID or path | | `tag_name` | string | Yes | Tag name of the release to delete | ### Webhooks [Section titled “Webhooks”](#webhooks) ## `gitlab_project_webhooks_list` [Section titled “gitlab\_project\_webhooks\_list”](#gitlab_project_webhooks_list) List all webhooks configured for a GitLab project. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | ## `gitlab_project_webhook_get` [Section titled “gitlab\_project\_webhook\_get”](#gitlab_project_webhook_get) Get details of a specific webhook including its URL, enabled triggers, and SSL verification setting. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `webhook_id` | number | Yes | Webhook ID | ## `gitlab_project_webhook_create` [Section titled “gitlab\_project\_webhook\_create”](#gitlab_project_webhook_create) Create a new webhook for a GitLab project. GitLab will send HTTP POST requests to the URL when the selected events occur. | Name | Type | Required | Description | | ------------------------- | ------- | -------- | ------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `url` | string | Yes | HTTPS endpoint to receive webhook payloads | | `token` | string | No | Secret token sent in the `X-Gitlab-Token` header for request verification | | `push_events` | boolean | No | Trigger on push events | | `issues_events` | boolean | No | Trigger on issue events | | `merge_requests_events` | boolean | No | Trigger on merge request events | | `tag_push_events` | boolean | No | Trigger on tag push events | | `pipeline_events` | boolean | No | Trigger on pipeline status changes | | `job_events` | boolean | No | Trigger on job status changes | | `releases_events` | boolean | No | Trigger on release events | | `enable_ssl_verification` | boolean | No | Verify the webhook endpoint’s SSL certificate. Defaults to `true` | ## `gitlab_project_webhook_update` [Section titled “gitlab\_project\_webhook\_update”](#gitlab_project_webhook_update) Update an existing webhook’s URL, token, or event triggers. | Name | Type | Required | Description | | ------------------------- | ------- | -------- | ----------------------------- | | `project_id` | string | Yes | Project ID or path | | `webhook_id` | number | Yes | Webhook ID | | `url` | string | No | New endpoint URL | | `token` | string | No | New secret token | | `push_events` | boolean | No | Update push event trigger | | `issues_events` | boolean | No | Update issues event trigger | | `merge_requests_events` | boolean | No | Update MR event trigger | | `pipeline_events` | boolean | No | Update pipeline event trigger | | `enable_ssl_verification` | boolean | No | Update SSL verification | ## `gitlab_project_webhook_delete` [Section titled “gitlab\_project\_webhook\_delete”](#gitlab_project_webhook_delete) Delete a webhook from a GitLab project. The endpoint will stop receiving events immediately. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `webhook_id` | number | Yes | Webhook ID | ### Deploy Keys [Section titled “Deploy Keys”](#deploy-keys) ## `gitlab_deploy_keys_list` [Section titled “gitlab\_deploy\_keys\_list”](#gitlab_deploy_keys_list) List all deploy keys configured for a GitLab project. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | ## `gitlab_deploy_key_create` [Section titled “gitlab\_deploy\_key\_create”](#gitlab_deploy_key_create) Create a new deploy key for a GitLab project. Deploy keys provide read-only (or read-write) SSH access to a single repository without user credentials — ideal for CI/CD systems and automation. | Name | Type | Required | Description | | ------------ | ------- | -------- | --------------------------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `title` | string | Yes | Label for the deploy key (e.g., `CI Server`) | | `key` | string | Yes | Full SSH public key string | | `can_push` | boolean | No | If `true`, the key has write (push) access. Defaults to `false` (read-only) | ## `gitlab_deploy_key_delete` [Section titled “gitlab\_deploy\_key\_delete”](#gitlab_deploy_key_delete) Delete a deploy key from a GitLab project. | Name | Type | Required | Description | | --------------- | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `deploy_key_id` | number | Yes | Deploy key ID | ### Snippets [Section titled “Snippets”](#snippets) ## `gitlab_project_snippets_list` [Section titled “gitlab\_project\_snippets\_list”](#gitlab_project_snippets_list) List all snippets in a GitLab project. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `page` | number | No | Page number | | `per_page` | number | No | Results per page | ## `gitlab_project_snippet_get` [Section titled “gitlab\_project\_snippet\_get”](#gitlab_project_snippet_get) Get a specific snippet from a GitLab project, including its title, description, and file names. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------ | | `project_id` | string | Yes | Project ID or path | | `snippet_id` | number | Yes | Snippet ID | ## `gitlab_project_snippet_create` [Section titled “gitlab\_project\_snippet\_create”](#gitlab_project_snippet_create) Create a new code snippet in a GitLab project. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------------------------- | | `project_id` | string | Yes | Project ID or path | | `title` | string | Yes | Snippet title | | `file_name` | string | No | File name (used for syntax highlighting) | | `content` | string | Yes | Snippet content | | `description` | string | No | Snippet description | | `visibility` | string | No | `public`, `internal`, or `private`. Defaults to `private` | --- # DOCUMENT BOUNDARY --- # Gmail > Gmail is Google's cloud based email service that allows you to access your messages from any computer or device with just a web browser. Gmail is Google’s cloud based email service that allows you to access your messages from any computer or device with just a web browser. ![Gmail logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/gmail.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Gmail connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: Caution Google applications using scopes that permit access to certain user data must complete a verification process. 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Gmail** and click **Create**. Note By default, a connection using Scalekit’s credentials will be created. If you are testing, go directly to the Usage section. Before going to production, update your connection by following the steps below. * Click **Use your own credentials** and copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.BSG_TC-7.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Navigate to [Google Cloud Console](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project) → **APIs & Services** → **Credentials**. Click **+ Create Credentials**, then **OAuth client ID**. Choose **Web application** as the application type. ![Select Web application in Google Cloud Console](/.netlify/images?url=_astro%2Foauth-web-app.DC96RwBt.png\&w=1100\&h=460\&dpl=69cce21a4f77360008b1503a) * Under **Authorized redirect URIs**, click **+ Add URI**, paste the redirect URI, and click **Create**. ![Add authorized redirect URI in Google Cloud Console](/.netlify/images?url=_astro%2Fadd-redirect-uri.B87wrMK8.png\&w=1504\&h=704\&dpl=69cce21a4f77360008b1503a) 2. ### Enable Gmail API * In [Google Cloud Console](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project), go to **APIs & Services** → **Library**. Search for “Gmail API” and click **Enable**. ![Enable Gmail API in Google Cloud Console](/.netlify/images?url=_astro%2Fenable-gmail-api.8vaJArEG.png\&w=996\&h=496\&dpl=69cce21a4f77360008b1503a) 3. ### Get client credentials Google provides your Client ID and Client Secret after you create the OAuth client ID in step 1. 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from above) * Client Secret (from above) * Permissions (scopes beginning with `gmail` — see [Google API Scopes reference](https://developers.google.com/identity/protocols/oauth2/scopes)) ![Add credentials for Gmail in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Gmail account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Gmail in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'gmail'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Gmail:', link); // present this link to your user for authorization, or click it yourself for testing 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/gmail/v1/users/me/profile', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "gmail" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Gmail:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/gmail/v1/users/me/profile", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `gmail_fetch_mails` [Section titled “gmail\_fetch\_mails”](#gmail_fetch_mails) Fetch emails from a connected Gmail account using search filters. Requires a valid Gmail OAuth2 connection. | Name | Type | Required | Description | | -------------------- | --------------- | -------- | ------------------------------------------------------------------------------------------ | | `format` | string | No | Format of the returned message. | | `include_spam_trash` | boolean | No | Whether to fetch emails from spam and trash folders | | `label_ids` | `array` | No | Gmail label IDs to filter messages | | `max_results` | integer | No | Maximum number of emails to fetch | | `page_token` | string | No | Page token for pagination | | `query` | string | No | Search query string using Gmail’s search syntax (e.g., ‘is:unread from:user\@example.com’) | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for execution | ## `gmail_get_attachment_by_id` [Section titled “gmail\_get\_attachment\_by\_id”](#gmail_get_attachment_by_id) Retrieve a specific attachment from a Gmail message using the message ID and attachment ID. | Name | Type | Required | Description | | ---------------- | ------ | -------- | -------------------------------------------------------------- | | `attachment_id` | string | Yes | Unique Gmail attachment ID | | `file_name` | string | No | Preferred filename to use when saving/returning the attachment | | `message_id` | string | Yes | Unique Gmail message ID that contains the attachment | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for execution | ## `gmail_get_contacts` [Section titled “gmail\_get\_contacts”](#gmail_get_contacts) Fetch a list of contacts from the connected Gmail account. Supports pagination and field filtering. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ------------------------------------------------- | | `max_results` | integer | No | Maximum number of contacts to fetch | | `page_token` | string | No | Token to retrieve the next page of results | | `person_fields` | `array` | No | Fields to include for each person | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for execution | ## `gmail_get_message_by_id` [Section titled “gmail\_get\_message\_by\_id”](#gmail_get_message_by_id) Retrieve a specific Gmail message using its message ID. Optionally control the format of the returned data. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------------------------- | | `format` | string | No | Format of the returned message. | | `message_id` | string | Yes | Unique Gmail message ID | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for execution | ## `gmail_get_thread_by_id` [Section titled “gmail\_get\_thread\_by\_id”](#gmail_get_thread_by_id) Retrieve a specific Gmail thread by thread ID. Optionally control message format and metadata headers. Requires a valid Gmail OAuth2 connection with read access. | Name | Type | Required | Description | | ------------------ | --------------- | -------- | --------------------------------------------------------- | | `format` | string | No | Format of messages in the returned thread. | | `metadata_headers` | `array` | No | Specific email headers to include when format is metadata | | `schema_version` | string | No | Optional schema version to use for tool execution | | `thread_id` | string | Yes | Unique Gmail thread ID | | `tool_version` | string | No | Optional tool version to use for execution | ## `gmail_list_drafts` [Section titled “gmail\_list\_drafts”](#gmail_list_drafts) List draft emails from a connected Gmail account. Requires a valid Gmail OAuth2 connection. | Name | Type | Required | Description | | ---------------- | ------- | -------- | ------------------------------------------------- | | `max_results` | integer | No | Maximum number of drafts to fetch | | `page_token` | string | No | Page token for pagination | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for execution | ## `gmail_list_threads` [Section titled “gmail\_list\_threads”](#gmail_list_threads) List threads in a connected Gmail account using optional search and label filters. Requires a valid Gmail OAuth2 connection with read access. | Name | Type | Required | Description | | -------------------- | --------------- | -------- | ----------------------------------------------------------------------------------------------- | | `include_spam_trash` | boolean | No | Whether to include threads from Spam and Trash | | `label_ids` | `array` | No | Gmail label IDs to filter threads (threads must match all labels) | | `max_results` | integer | No | Maximum number of threads to return | | `page_token` | string | No | Page token for pagination | | `query` | string | No | Search query string using Gmail search syntax (for example, ‘is:unread from:user\@example.com’) | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for execution | ## `gmail_search_people` [Section titled “gmail\_search\_people”](#gmail_search_people) Search people or contacts in the connected Google account using a query. Requires a valid Google OAuth2 connection with People API scopes. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ----------------------------------------------------------------------------- | | `other_contacts` | boolean | No | Whether to include people not in the user’s contacts (from ‘Other Contacts’). | | `page_size` | integer | No | Maximum number of people to return. | | `person_fields` | `array` | No | Fields to retrieve for each person. | | `query` | string | Yes | Text query to search people (e.g., name, email address). | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for execution | --- # DOCUMENT BOUNDARY --- # Gong > Connect with Gong to sync calls, transcripts, insights, coaching and CRM activity Connect with Gong to sync calls, transcripts, insights, coaching and CRM activity ![Gong logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/gong.svg) Supports authentication: OAuth 2.0 , Api Key ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Gong connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. You’ll need your app credentials from the Gong Developer Portal. 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. * Find **Gong** from the list of providers and click **Create**. Note By default, a connection using Scalekit’s credentials will be created. If you are testing, go directly to the next section. Before going to production, update your connection by following the steps below. * Click **Use your own credentials** and copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.CcXwmr6T.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * In the [Gong Developer Portal](https://app.gong.io/settings/api/documentation#overview), open your app. * Paste the copied URI into the **Redirect URL** field and click **Save**. ![Add redirect URL in Gong Developer Portal](/.netlify/images?url=_astro%2Fadd-redirect-uri.Dm2xo3R_.png\&w=1440\&h=720\&dpl=69cce21a4f77360008b1503a) 2. ### Get client credentials * In the [Gong Developer Portal](https://app.gong.io/settings/api/documentation#overview), open your app: * **Client ID** — listed under **Client ID** * **Client Secret** — listed under **Client Secret** 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from your Gong app) * Client Secret (from your Gong app) * Permissions — select the scopes your app needs ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Gong account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Gong in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'gong'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Gong:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v2/users', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "gong" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Gong:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v2/users", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Google Ads > Connect to Google Ads to manage advertising campaigns, analyze performance metrics, and optimize ad spending across Google's advertising platform Connect to Google Ads to manage advertising campaigns, analyze performance metrics, and optimize ad spending across Google’s advertising platform ![Google Ads logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/google_ads.png) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Google Ads connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: Caution Google applications using scopes that permit access to certain user data must complete a verification process. 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Google Ads** and click **Create**. Click **Use your own credentials** and copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.CxPlnUgs.png\&w=1280\&h=832\&dpl=69cce21a4f77360008b1503a) * Navigate to [Google Cloud Console](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project) → **APIs & Services** → **Credentials**. Select **+ Create Credentials**, then **OAuth client ID**. Choose **Web application** from the Application type menu. ![Select Web Application in Google OAuth settings](/.netlify/images?url=_astro%2Foauth-web-app.DC96RwBt.png\&w=1100\&h=460\&dpl=69cce21a4f77360008b1503a) * Under **Authorized redirect URIs**, click **+ Add URI**, paste the redirect URI, and click **Create**. ![Add authorized redirect URI in Google Cloud Console](/.netlify/images?url=_astro%2Fadd-redirect-uri.B87wrMK8.png\&w=1504\&h=704\&dpl=69cce21a4f77360008b1503a) 2. ### Enable the Google Ads API * In [Google Cloud Console](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project), go to **APIs & Services** → **Library**. Search for “Google Ads API” and click **Enable**. 3. ### Get client credentials * Google provides your Client ID and Client Secret after you create the OAuth client ID in step 1. 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from above) * Client Secret (from above) * Permissions (scopes — see [Google API Scopes reference](https://developers.google.com/identity/protocols/oauth2/scopes)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Google Ads account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Google Ads in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'google_ads'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Google Ads:', link); // present this link to your user for authorization, or click it yourself for testing 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v17/customers', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "google_ads" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Google Ads:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v17/customers", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Google Calendar > Google Calendar is Google's cloud-based calendar service that allows you to manage your events, appointments, and schedules from any computer or device with just a web browser. Google Calendar is Google’s cloud-based calendar service that allows you to manage your events, appointments, and schedules from any computer or device with just a web browser. ![Google Calendar logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/google_calendar.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Google Calendar connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: Caution Google applications using scopes that permit access to certain user data must complete a verification process. 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Google Calendar** and click **Create**. Click **Use your own credentials** and copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.BMTotywz.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Navigate to [Google Cloud Console](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project) → **APIs & Services** → **Credentials**. Select **+ Create Credentials**, then **OAuth client ID**. Choose **Web application** from the Application type menu. ![Select Web Application in Google OAuth settings](/.netlify/images?url=_astro%2Foauth-web-app.DC96RwBt.png\&w=1100\&h=460\&dpl=69cce21a4f77360008b1503a) * Under **Authorized redirect URIs**, click **+ Add URI**, paste the redirect URI, and click **Create**. ![Add authorized redirect URI in Google Cloud Console](/.netlify/images?url=_astro%2Fadd-redirect-uri.B87wrMK8.png\&w=1504\&h=704\&dpl=69cce21a4f77360008b1503a) 2. ### Enable the Google Calendar API * In [Google Cloud Console](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project), go to **APIs & Services** → **Library**. Search for “Google Calendar API” and click **Enable**. 3. ### Get client credentials * Google provides your Client ID and Client Secret after you create the OAuth client ID in step 1. 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from above) * Client Secret (from above) * Permissions (scopes — see [Google API Scopes reference](https://developers.google.com/identity/protocols/oauth2/scopes)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Google Calendar account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Google Calendar in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'googlecalendar'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Google Calendar:', link); // present this link to your user for authorization, or click it yourself for testing 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/calendar/v3/users/me/calendarList', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "googlecalendar" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Google Calendar:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/calendar/v3/users/me/calendarList", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `googlecalendar_create_event` [Section titled “googlecalendar\_create\_event”](#googlecalendar_create_event) Create a new event in a connected Google Calendar account. Supports meeting links, recurrence, attendees, and more. | Name | Type | Required | Description | | ----------------------------- | --------------- | -------- | -------------------------------------------------- | | `attendees_emails` | `array` | No | Attendee email addresses | | `calendar_id` | string | No | Calendar ID to create the event in | | `create_meeting_room` | boolean | No | Generate a Google Meet link for this event | | `description` | string | No | Optional event description | | `event_duration_hour` | integer | No | Duration of event in hours | | `event_duration_minutes` | integer | No | Duration of event in minutes | | `event_type` | string | No | Event type for display purposes | | `guests_can_invite_others` | boolean | No | Allow guests to invite others | | `guests_can_modify` | boolean | No | Allow guests to modify the event | | `guests_can_see_other_guests` | boolean | No | Allow guests to see each other | | `location` | string | No | Location of the event | | `recurrence` | `array` | No | Recurrence rules (iCalendar RRULE format) | | `schema_version` | string | No | Optional schema version to use for tool execution | | `send_updates` | boolean | No | Send update notifications to attendees | | `start_datetime` | string | Yes | Event start time in RFC3339 format | | `summary` | string | Yes | Event title/summary | | `timezone` | string | No | Timezone for the event (IANA time zone identifier) | | `tool_version` | string | No | Optional tool version to use for execution | | `transparency` | string | No | Calendar transparency (free/busy) | | `visibility` | string | No | Visibility of the event | ## `googlecalendar_delete_event` [Section titled “googlecalendar\_delete\_event”](#googlecalendar_delete_event) Delete an event from a connected Google Calendar account. Requires the calendar ID and event ID. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------------------------------------- | | `calendar_id` | string | No | The ID of the calendar from which the event should be deleted | | `event_id` | string | Yes | The ID of the calendar event to delete | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for execution | ## `googlecalendar_get_event_by_id` [Section titled “googlecalendar\_get\_event\_by\_id”](#googlecalendar_get_event_by_id) Retrieve a specific calendar event by its ID using optional filtering and list parameters. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ---------------------------------------------------- | | `calendar_id` | string | No | The calendar ID to search in | | `event_id` | string | Yes | The unique identifier of the calendar event to fetch | | `event_types` | `array` | No | Filter by Google event types | | `query` | string | No | Free text search query | | `schema_version` | string | No | Optional schema version to use for tool execution | | `show_deleted` | boolean | No | Include deleted events in results | | `single_events` | boolean | No | Expand recurring events into instances | | `time_max` | string | No | Upper bound for event start time (RFC3339) | | `time_min` | string | No | Lower bound for event start time (RFC3339) | | `tool_version` | string | No | Optional tool version to use for execution | | `updated_min` | string | No | Filter events updated after this time (RFC3339) | ## `googlecalendar_list_calendars` [Section titled “googlecalendar\_list\_calendars”](#googlecalendar_list_calendars) List all accessible Google Calendar calendars for the authenticated user. Supports filters and pagination. | Name | Type | Required | Description | | ----------------- | ------- | -------- | -------------------------------------------------------- | | `max_results` | integer | No | Maximum number of calendars to fetch | | `min_access_role` | string | No | Minimum access role to include in results | | `page_token` | string | No | Token to retrieve the next page of results | | `schema_version` | string | No | Optional schema version to use for tool execution | | `show_deleted` | boolean | No | Include deleted calendars in the list | | `show_hidden` | boolean | No | Include calendars that are hidden from the calendar list | | `sync_token` | string | No | Token to get updates since the last sync | | `tool_version` | string | No | Optional tool version to use for execution | ## `googlecalendar_list_events` [Section titled “googlecalendar\_list\_events”](#googlecalendar_list_events) List events from a connected Google Calendar account with filtering options. Requires a valid Google Calendar OAuth2 connection. | Name | Type | Required | Description | | ---------------- | ------- | -------- | ---------------------------------------------------- | | `calendar_id` | string | No | Calendar ID to list events from | | `max_results` | integer | No | Maximum number of events to fetch | | `order_by` | string | No | Order of events in the result | | `page_token` | string | No | Page token for pagination | | `query` | string | No | Free text search query | | `schema_version` | string | No | Optional schema version to use for tool execution | | `single_events` | boolean | No | Expand recurring events into single events | | `time_max` | string | No | Upper bound for event start time (RFC3339 timestamp) | | `time_min` | string | No | Lower bound for event start time (RFC3339 timestamp) | | `tool_version` | string | No | Optional tool version to use for execution | ## `googlecalendar_update_event` [Section titled “googlecalendar\_update\_event”](#googlecalendar_update_event) Update an existing event in a connected Google Calendar account. Only provided fields will be updated. Supports updating time, attendees, location, meeting links, and more. | Name | Type | Required | Description | | ----------------------------- | --------------- | -------- | -------------------------------------------------- | | `attendees_emails` | `array` | No | Attendee email addresses | | `calendar_id` | string | Yes | Calendar ID containing the event | | `create_meeting_room` | boolean | No | Generate a Google Meet link for this event | | `description` | string | No | Optional event description | | `end_datetime` | string | No | Event end time in RFC3339 format | | `event_duration_hour` | integer | No | Duration of event in hours | | `event_duration_minutes` | integer | No | Duration of event in minutes | | `event_id` | string | Yes | The ID of the calendar event to update | | `event_type` | string | No | Event type for display purposes | | `guests_can_invite_others` | boolean | No | Allow guests to invite others | | `guests_can_modify` | boolean | No | Allow guests to modify the event | | `guests_can_see_other_guests` | boolean | No | Allow guests to see each other | | `location` | string | No | Location of the event | | `recurrence` | `array` | No | Recurrence rules (iCalendar RRULE format) | | `schema_version` | string | No | Optional schema version to use for tool execution | | `send_updates` | boolean | No | Send update notifications to attendees | | `start_datetime` | string | No | Event start time in RFC3339 format | | `summary` | string | No | Event title/summary | | `timezone` | string | No | Timezone for the event (IANA time zone identifier) | | `tool_version` | string | No | Optional tool version to use for execution | | `transparency` | string | No | Calendar transparency (free/busy) | | `visibility` | string | No | Visibility of the event | --- # DOCUMENT BOUNDARY --- # Google Docs > Connect to Google Docs. Create, edit, and collaborate on documents Connect to Google Docs. Create, edit, and collaborate on documents ![Google Docs logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/google_docs.svg) Supports authentication: OAuth 2.0 ## Usage [Section titled “Usage”](#usage) Connect a user’s Google Docs account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Google Docs in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'google_docs'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Google Docs:', link); // present this link to your user for authorization, or click it yourself for testing 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1/documents', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "google_docs" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Google Docs:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1/documents", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## `googledocs_create_document` Create a new blank Google Doc with an optional title. Returns the new document’s ID and metadata. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------------------------- | | `schema_version` | string | No | Optional schema version to use for tool execution | | `title` | string | No | Title of the new document | | `tool_version` | string | No | Optional tool version to use for execution | ## `googledocs_read_document` Read the complete content and structure of a Google Doc including text, formatting, tables, and metadata. | Name | Type | Required | Description | | ----------------------- | ------ | -------- | ------------------------------------------------- | | `document_id` | string | Yes | The ID of the Google Doc to read | | `schema_version` | string | No | Optional schema version to use for tool execution | | `suggestions_view_mode` | string | No | How suggestions are rendered in the response | | `tool_version` | string | No | Optional tool version to use for execution | ## `googledocs_update_document` Update the content of an existing Google Doc using batch update requests. Supports inserting and deleting text, formatting, tables, and other document elements. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ------------------------------------------------- | | `document_id` | string | Yes | The ID of the Google Doc to update | | `requests` | `array` | Yes | Array of update requests to apply to the document | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for execution | | `write_control` | `object` | No | Optional write control for revision management | ## Tool list [Section titled “Tool list”](#tool-list) ## `googledocs_create_document` [Section titled “googledocs\_create\_document”](#googledocs_create_document) Create a new blank Google Doc with an optional title. Returns the new document’s ID and metadata. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------------------------- | | `schema_version` | string | No | Optional schema version to use for tool execution | | `title` | string | No | Title of the new document | | `tool_version` | string | No | Optional tool version to use for execution | ## `googledocs_read_document` [Section titled “googledocs\_read\_document”](#googledocs_read_document) Read the complete content and structure of a Google Doc including text, formatting, tables, and metadata. | Name | Type | Required | Description | | ----------------------- | ------ | -------- | ------------------------------------------------- | | `document_id` | string | Yes | The ID of the Google Doc to read | | `schema_version` | string | No | Optional schema version to use for tool execution | | `suggestions_view_mode` | string | No | How suggestions are rendered in the response | | `tool_version` | string | No | Optional tool version to use for execution | ## `googledocs_update_document` [Section titled “googledocs\_update\_document”](#googledocs_update_document) Update the content of an existing Google Doc using batch update requests. Supports inserting and deleting text, formatting, tables, and other document elements. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ------------------------------------------------- | | `document_id` | string | Yes | The ID of the Google Doc to update | | `requests` | `array` | Yes | Array of update requests to apply to the document | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for execution | | `write_control` | `object` | No | Optional write control for revision management | --- # DOCUMENT BOUNDARY --- # Google Drive > Connect to Google Drive. Manage files, folders, and sharing permissions Connect to Google Drive. Manage files, folders, and sharing permissions ![Google Drive logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/google_drive.svg) Supports authentication: OAuth 2.0 ## Usage [Section titled “Usage”](#usage) Connect a user’s Google Drive account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Google Drive in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'google_drive'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Google Drive:', link); // present this link to your user for authorization, or click it yourself for testing 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/drive/v3/files', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "google_drive" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Google Drive:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/drive/v3/files", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## File operations ### Download a file Download a file from Google Drive by its file ID via the Scalekit proxy. * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "google_drive" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 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_ENV_URL"), 12 ) 13 14 file_id = "" # file ID from Drive (visible in the file's URL) 15 output_path = "downloaded.pdf" 16 17 response = scalekit_client.actions.request( 18 connection_name=connection_name, 19 identifier=identifier, 20 path=f"/drive/v3/files/{file_id}", 21 method="GET", 22 query_params={"alt": "media"}, 23 ) 24 25 with open(output_path, "wb") as f: 26 f.write(response.content) 27 28 print(f"Downloaded: {output_path} ({len(response.content):,} bytes)") ``` ### Upload a file Upload a file to Google Drive via the Scalekit proxy. Scalekit injects the OAuth token automatically — your app never handles credentials directly. * Python ```python 1 import mimetypes 2 import scalekit.client, os 3 from dotenv import load_dotenv 4 load_dotenv() 5 6 connection_name = "google_drive" # get your connection name from connection configurations 7 identifier = "user_123" # your unique user identifier 8 9 scalekit_client = 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_ENV_URL"), 13 ) 14 15 file_path = "report.pdf" 16 file_name = "report.pdf" 17 18 with open(file_path, "rb") as f: 19 file_bytes = f.read() 20 21 mime_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream" 22 23 response = scalekit_client.actions.request( 24 connection_name=connection_name, 25 identifier=identifier, 26 path="/upload/drive/v3/files", 27 method="POST", 28 query_params={"uploadType": "media", "name": file_name}, 29 form_data=file_bytes, 30 headers={"Content-Type": mime_type}, 31 ) 32 33 file_id = response.json()["id"] 34 print(f"Uploaded: {file_name} (File ID: {file_id})") ``` ## `googledrive_get_file_metadata` Retrieve metadata for a specific file in Google Drive by its file ID. Returns name, MIME type, size, creation time, and more. | Name | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------- | | `fields` | string | No | Fields to include in the response | | `file_id` | string | Yes | The ID of the file to retrieve metadata for | | `schema_version` | string | No | Optional schema version to use for tool execution | | `supports_all_drives` | boolean | No | Support shared drives | | `tool_version` | string | No | Optional tool version to use for execution | ## `googledrive_search_content` Search inside the content of files stored in Google Drive using full-text search. Finds files where the body text matches the search term. | Name | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------- | | `fields` | string | No | Fields to include in the response | | `mime_type` | string | No | Filter results by MIME type | | `page_size` | integer | No | Number of files to return per page | | `page_token` | string | No | Token for the next page of results | | `schema_version` | string | No | Optional schema version to use for tool execution | | `search_term` | string | Yes | Text to search for inside file contents | | `supports_all_drives` | boolean | No | Include shared drives in results | | `tool_version` | string | No | Optional tool version to use for execution | ## `googledrive_search_files` Search for files and folders in Google Drive using query filters like name, type, owner, and parent folder. | Name | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------- | | `fields` | string | No | Fields to include in the response | | `order_by` | string | No | Sort order for results | | `page_size` | integer | No | Number of files to return per page | | `page_token` | string | No | Token for the next page of results | | `query` | string | No | Drive search query string | | `schema_version` | string | No | Optional schema version to use for tool execution | | `supports_all_drives` | boolean | No | Include shared drives in results | | `tool_version` | string | No | Optional tool version to use for execution | ## Tool list [Section titled “Tool list”](#tool-list) ## `googledrive_get_file_metadata` [Section titled “googledrive\_get\_file\_metadata”](#googledrive_get_file_metadata) Retrieve metadata for a specific file in Google Drive by its file ID. Returns name, MIME type, size, creation time, and more. | Name | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------- | | `fields` | string | No | Fields to include in the response | | `file_id` | string | Yes | The ID of the file to retrieve metadata for | | `schema_version` | string | No | Optional schema version to use for tool execution | | `supports_all_drives` | boolean | No | Support shared drives | | `tool_version` | string | No | Optional tool version to use for execution | ## `googledrive_search_content` [Section titled “googledrive\_search\_content”](#googledrive_search_content) Search inside the content of files stored in Google Drive using full-text search. Finds files where the body text matches the search term. | Name | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------- | | `fields` | string | No | Fields to include in the response | | `mime_type` | string | No | Filter results by MIME type | | `page_size` | integer | No | Number of files to return per page | | `page_token` | string | No | Token for the next page of results | | `schema_version` | string | No | Optional schema version to use for tool execution | | `search_term` | string | Yes | Text to search for inside file contents | | `supports_all_drives` | boolean | No | Include shared drives in results | | `tool_version` | string | No | Optional tool version to use for execution | ## `googledrive_search_files` [Section titled “googledrive\_search\_files”](#googledrive_search_files) Search for files and folders in Google Drive using query filters like name, type, owner, and parent folder. | Name | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------- | | `fields` | string | No | Fields to include in the response | | `order_by` | string | No | Sort order for results | | `page_size` | integer | No | Number of files to return per page | | `page_token` | string | No | Token for the next page of results | | `query` | string | No | Drive search query string | | `schema_version` | string | No | Optional schema version to use for tool execution | | `supports_all_drives` | boolean | No | Include shared drives in results | | `tool_version` | string | No | Optional tool version to use for execution | --- # DOCUMENT BOUNDARY --- # Google Forms > Connect to Google Forms. Create, view, and manage forms and responses securely Connect to Google Forms. Create, view, and manage forms and responses securely ![Google Forms logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/google_forms.svg) Supports authentication: OAuth 2.0 ## Usage [Section titled “Usage”](#usage) Connect a user’s Google Forms account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Google Forms in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'google_forms'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Google Forms:', link); // present this link to your user for authorization, or click it yourself for testing 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1/forms', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "google_forms" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Google Forms:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1/forms", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Google Meet > Connect to Google Meet. Create and manage video meetings with powerful collaboration features Connect to Google Meet. Create and manage video meetings with powerful collaboration features ![Google Meet logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/google_meet.svg) Supports authentication: OAuth 2.0 ## Usage [Section titled “Usage”](#usage) Connect a user’s Google Meet account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Google Meet in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'google_meets'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Google Meet:', link); // present this link to your user for authorization, or click it yourself for testing 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v2/spaces', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "google_meets" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Google Meet:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v2/spaces", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Google Sheets > Connect to Google Sheets. Create, edit, and analyze spreadsheets with powerful data management capabilities Connect to Google Sheets. Create, edit, and analyze spreadsheets with powerful data management capabilities ![Google Sheets logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/google_sheets.svg) Supports authentication: OAuth 2.0 ## Usage [Section titled “Usage”](#usage) Connect a user’s Google Sheets account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Google Sheets in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'google_sheets'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Google Sheets:', link); // present this link to your user for authorization, or click it yourself for testing 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v4/spreadsheets', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "google_sheets" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Google Sheets:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v4/spreadsheets", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## `googlesheets_create_spreadsheet` Create a new Google Sheets spreadsheet with an optional title and initial sheet configuration. Returns the new spreadsheet ID and metadata. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ------------------------------------------------- | | `locale` | string | No | Locale of the spreadsheet | | `schema_version` | string | No | Optional schema version to use for tool execution | | `sheets` | `array` | No | Initial sheets to include in the spreadsheet | | `time_zone` | string | No | Time zone for the spreadsheet | | `title` | string | No | Title of the new spreadsheet | | `tool_version` | string | No | Optional tool version to use for execution | ## `googlesheets_get_values` Returns only the cell values from a specific range in a Google Sheet — no metadata, no formatting, just the data. For full spreadsheet metadata and formatting, use googlesheets\_read\_spreadsheet instead. | Name | Type | Required | Description | | --------------------- | ------ | -------- | ------------------------------------------------- | | `major_dimension` | string | No | Whether values are returned by rows or columns | | `range` | string | Yes | Cell range to read in A1 notation | | `schema_version` | string | No | Optional schema version to use for tool execution | | `spreadsheet_id` | string | Yes | The ID of the Google Sheet | | `tool_version` | string | No | Optional tool version to use for execution | | `value_render_option` | string | No | How values should be rendered in the response | ## `googlesheets_read_spreadsheet` Returns everything about a spreadsheet — including spreadsheet metadata, sheet properties, cell values, formatting, themes, and pixel sizes. If you only need cell values, use googlesheets\_get\_values instead. | Name | Type | Required | Description | | ------------------- | ------- | -------- | ------------------------------------------------- | | `include_grid_data` | boolean | No | Include cell data in the response | | `ranges` | string | No | Cell range to read in A1 notation | | `schema_version` | string | No | Optional schema version to use for tool execution | | `spreadsheet_id` | string | Yes | The ID of the Google Sheet to read | | `tool_version` | string | No | Optional tool version to use for execution | ## `googlesheets_update_values` Update cell values in a specific range of a Google Sheet. Supports writing single cells or multiple rows and columns at once. | Name | Type | Required | Description | | ---------------------------- | -------------- | -------- | ------------------------------------------------- | | `include_values_in_response` | boolean | No | Return the updated cell values in the response | | `range` | string | Yes | Cell range to update in A1 notation | | `schema_version` | string | No | Optional schema version to use for tool execution | | `spreadsheet_id` | string | Yes | The ID of the Google Sheet to update | | `tool_version` | string | No | Optional tool version to use for execution | | `value_input_option` | string | No | How input values should be interpreted | | `values` | `array` | Yes | 2D array of values to write to the range | ## Tool list [Section titled “Tool list”](#tool-list) ## `googlesheets_create_spreadsheet` [Section titled “googlesheets\_create\_spreadsheet”](#googlesheets_create_spreadsheet) Create a new Google Sheets spreadsheet with an optional title and initial sheet configuration. Returns the new spreadsheet ID and metadata. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ------------------------------------------------- | | `locale` | string | No | Locale of the spreadsheet | | `schema_version` | string | No | Optional schema version to use for tool execution | | `sheets` | `array` | No | Initial sheets to include in the spreadsheet | | `time_zone` | string | No | Time zone for the spreadsheet | | `title` | string | No | Title of the new spreadsheet | | `tool_version` | string | No | Optional tool version to use for execution | ## `googlesheets_get_values` [Section titled “googlesheets\_get\_values”](#googlesheets_get_values) Returns only the cell values from a specific range in a Google Sheet — no metadata, no formatting, just the data. For full spreadsheet metadata and formatting, use googlesheets\_read\_spreadsheet instead. | Name | Type | Required | Description | | --------------------- | ------ | -------- | ------------------------------------------------- | | `major_dimension` | string | No | Whether values are returned by rows or columns | | `range` | string | Yes | Cell range to read in A1 notation | | `schema_version` | string | No | Optional schema version to use for tool execution | | `spreadsheet_id` | string | Yes | The ID of the Google Sheet | | `tool_version` | string | No | Optional tool version to use for execution | | `value_render_option` | string | No | How values should be rendered in the response | ## `googlesheets_read_spreadsheet` [Section titled “googlesheets\_read\_spreadsheet”](#googlesheets_read_spreadsheet) Returns everything about a spreadsheet — including spreadsheet metadata, sheet properties, cell values, formatting, themes, and pixel sizes. If you only need cell values, use googlesheets\_get\_values instead. | Name | Type | Required | Description | | ------------------- | ------- | -------- | ------------------------------------------------- | | `include_grid_data` | boolean | No | Include cell data in the response | | `ranges` | string | No | Cell range to read in A1 notation | | `schema_version` | string | No | Optional schema version to use for tool execution | | `spreadsheet_id` | string | Yes | The ID of the Google Sheet to read | | `tool_version` | string | No | Optional tool version to use for execution | ## `googlesheets_update_values` [Section titled “googlesheets\_update\_values”](#googlesheets_update_values) Update cell values in a specific range of a Google Sheet. Supports writing single cells or multiple rows and columns at once. | Name | Type | Required | Description | | ---------------------------- | -------------- | -------- | ------------------------------------------------- | | `include_values_in_response` | boolean | No | Return the updated cell values in the response | | `range` | string | Yes | Cell range to update in A1 notation | | `schema_version` | string | No | Optional schema version to use for tool execution | | `spreadsheet_id` | string | Yes | The ID of the Google Sheet to update | | `tool_version` | string | No | Optional tool version to use for execution | | `value_input_option` | string | No | How input values should be interpreted | | `values` | `array` | Yes | 2D array of values to write to the range | --- # DOCUMENT BOUNDARY --- # Google Slides > Connect to Google Slides to create, read, and modify presentations programmatically. Connect to Google Slides to create, read, and modify presentations programmatically. ![Google Slides logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/google_slides.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Google Slides connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: Caution Google applications using scopes that permit access to certain user data must complete a verification process. 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Google Slides** and click **Create**. Click **Use your own credentials** and copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.Di-jft2E.png\&w=1280\&h=832\&dpl=69cce21a4f77360008b1503a) * Navigate to [Google Cloud Console](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project) → **APIs & Services** → **Credentials**. Select **+ Create Credentials**, then **OAuth client ID**. Choose **Web application** from the Application type menu. ![Select Web Application in Google OAuth settings](/.netlify/images?url=_astro%2Foauth-web-app.DC96RwBt.png\&w=1100\&h=460\&dpl=69cce21a4f77360008b1503a) * Under **Authorized redirect URIs**, click **+ Add URI**, paste the redirect URI, and click **Create**. ![Add authorized redirect URI in Google Cloud Console](/.netlify/images?url=_astro%2Fadd-redirect-uri.B87wrMK8.png\&w=1504\&h=704\&dpl=69cce21a4f77360008b1503a) 2. ### Enable the Google Slides API * In [Google Cloud Console](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project), go to **APIs & Services** → **Library**. Search for “Google Slides API” and click **Enable**. 3. ### Get client credentials * Google provides your Client ID and Client Secret after you create the OAuth client ID in step 1. 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from above) * Client Secret (from above) * Permissions (scopes — see [Google API Scopes reference](https://developers.google.com/identity/protocols/oauth2/scopes)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Google Slides account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Google Slides in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'google_slides'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Google Slides:', link); // present this link to your user for authorization, or click it yourself for testing 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1/presentations', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "google_slides" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Google Slides:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1/presentations", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `googleslides_create_presentation` [Section titled “googleslides\_create\_presentation”](#googleslides_create_presentation) Create a new Google Slides presentation with an optional title. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------------------------- | | `schema_version` | string | No | Optional schema version to use for tool execution | | `title` | string | No | Title of the new presentation | | `tool_version` | string | No | Optional tool version to use for execution | ## `googleslides_read_presentation` [Section titled “googleslides\_read\_presentation”](#googleslides_read_presentation) Read the complete structure and content of a Google Slides presentation including slides, text, images, shapes, and metadata. | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------- | | `fields` | string | No | Fields to include in the response | | `presentation_id` | string | Yes | The ID of the Google Slides presentation to read | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for execution | --- # DOCUMENT BOUNDARY --- # Granola MCP > Connect to Granola MCP to search meeting notes, inspect meeting details, and retrieve transcripts from Granola's official MCP server. Connect to Granola MCP to search meeting notes, inspect detailed meeting metadata, and retrieve verbatim meeting transcripts from Granola’s official MCP server. ![Granola MCP logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/granola.svg) Supports authentication: OAuth 2.1 What you can build with this connector | Use case | Tools involved | | ------------------------------------------------------- | ----------------------------------- | | **Ask natural-language questions across meeting notes** | `granolamcp_query_granola_meetings` | | **List meetings for a date range** | `granolamcp_list_meetings` | | **Fetch full details for specific meetings** | `granolamcp_get_meetings` | | **Retrieve exact spoken wording from a meeting** | `granolamcp_get_meeting_transcript` | **Key concepts:** * **Official MCP server**: This connector uses Granola’s hosted MCP endpoint at `https://mcp.granola.ai/mcp`. * **OAuth with browser sign-in**: Authentication uses Granola’s OAuth flow with PKCE and dynamic client registration. * **Read-only access**: The available tools retrieve meeting notes, summaries, metadata, and transcripts. They do not create or modify meetings. * **Query first for open-ended questions**: Prefer `granolamcp_query_granola_meetings` when the user asks broad questions about meeting content or decisions. ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) 1. In your agent connector settings, choose **Granola MCP**. 2. Start the connection flow and sign in to your Granola account in the browser. 3. Approve access to Granola’s official MCP server. 4. Return to the app once the OAuth flow completes. Authentication details Granola MCP uses OAuth with PKCE and dynamic client registration against Granola’s official MCP server. No API key or manual endpoint configuration is required for the standard connection flow. ## Usage [Section titled “Usage”](#usage) Connect a user’s Granola account and query Granola’s official MCP server through Scalekit. Scalekit handles the OAuth flow, token storage, and tool execution automatically. Granola MCP is primarily used through Scalekit tools. Use `scalekit_client.actions.execute_tool()` to search meeting notes, list meetings, fetch meeting details, and retrieve transcripts without calling the upstream MCP server directly. ## Tool Calling Use this connector when you want an agent to work with Granola meeting content, including summaries, notes, attendees, and transcripts. * Use `granolamcp_query_granola_meetings` for natural-language questions such as decisions, action items, or follow-ups from past meetings. * Use `granolamcp_list_meetings` to find meetings in a time window before drilling into specific meeting IDs. * Use `granolamcp_get_meetings` when you already know the Granola meeting IDs and need richer metadata or notes. * Use `granolamcp_get_meeting_transcript` when exact wording matters and you need the verbatim transcript instead of summarized notes. - Python examples/granolamcp\_query\_meetings.py ```python 1 import os 2 from scalekit.client import ScalekitClient 3 4 scalekit_client = ScalekitClient( 5 client_id=os.environ["SCALEKIT_CLIENT_ID"], 6 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 7 env_url=os.environ["SCALEKIT_ENV_URL"], 8 ) 9 10 auth_link = scalekit_client.actions.get_authorization_link( 11 connection_name="granolamcp", 12 identifier="user_123", 13 ) 14 print("Authorize Granola MCP:", auth_link.link) 15 input("Press Enter after authorizing...") 16 17 connected_account = scalekit_client.actions.get_or_create_connected_account( 18 connection_name="granolamcp", 19 identifier="user_123", 20 ) 21 22 tool_response = scalekit_client.actions.execute_tool( 23 tool_name="granolamcp_query_granola_meetings", 24 connected_account_id=connected_account.connected_account.id, 25 tool_input={ 26 "query": "What decisions and follow-ups came out of last week's customer calls?" 27 }, 28 ) 29 print("Granola response:", tool_response) ``` - Node.js examples/granolamcp\_query\_meetings.ts ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const scalekit = new ScalekitClient( 5 process.env.SCALEKIT_ENV_URL!, 6 process.env.SCALEKIT_CLIENT_ID!, 7 process.env.SCALEKIT_CLIENT_SECRET! 8 ); 9 const actions = scalekit.actions; 10 11 const { link } = await actions.getAuthorizationLink({ 12 connectionName: 'granolamcp', 13 identifier: 'user_123', 14 }); 15 console.log('Authorize Granola MCP:', link); 16 process.stdout.write('Press Enter after authorizing...'); 17 await new Promise((resolve) => process.stdin.once('data', resolve)); 18 19 const connectedAccount = await actions.getOrCreateConnectedAccount({ 20 connectionName: 'granolamcp', 21 identifier: 'user_123', 22 }); 23 24 const toolResponse = await actions.executeTool({ 25 toolName: 'granolamcp_query_granola_meetings', 26 connectedAccountId: connectedAccount?.id, 27 toolInput: { 28 query: "What decisions and follow-ups came out of last week's customer calls?", 29 }, 30 }); 31 console.log('Granola response:', toolResponse.data); ``` Preserve citations `granolamcp_query_granola_meetings` returns inline citations back to the source meeting notes. Keep those citations in your final user-facing response when possible. ## Tool list [Section titled “Tool list”](#tool-list) ## `granolamcp_get_meeting_transcript` [Section titled “granolamcp\_get\_meeting\_transcript”](#granolamcp_get_meeting_transcript) Get the full transcript for a specific Granola meeting by ID. Returns only the verbatim transcript content, not summaries or notes. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------------------------- | | `meeting_id` | string | Yes | Meeting UUID | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for tool execution | ## `granolamcp_get_meetings` [Section titled “granolamcp\_get\_meetings”](#granolamcp_get_meetings) Get detailed meeting information for one or more Granola meetings by ID. Returns private notes, AI-generated summary, attendees, and metadata. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ------------------------------------------------- | | `meeting_ids` | `array` | Yes | Array of meeting UUIDs (max 10) | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for tool execution | ## `granolamcp_list_meetings` [Section titled “granolamcp\_list\_meetings”](#granolamcp_list_meetings) List the user’s Granola meeting notes within a time range. Returns meeting titles and metadata. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------- | | `custom_end` | string | No | ISO date for custom range end. Required if `time_range` is `custom`. | | `custom_start` | string | No | ISO date for custom range start. Required if `time_range` is `custom`. | | `schema_version` | string | No | Optional schema version to use for tool execution | | `time_range` | string | No | Time range to query meetings from. One of `this_week`, `last_week`, `last_30_days`, or `custom`. Defaults to `last_30_days`. | | `tool_version` | string | No | Optional tool version to use for tool execution | ## `granolamcp_query_granola_meetings` [Section titled “granolamcp\_query\_granola\_meetings”](#granolamcp_query_granola_meetings) Query Granola about the user’s meetings using natural language. Returns a tailored response with inline citations that reference source meeting notes. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | --------------------------------------------------------- | | `document_ids` | `array` | No | Optional list of specific meeting IDs to limit context to | | `query` | string | Yes | The query to run on Granola meeting notes | | `schema_version` | string | No | Optional schema version to use for tool execution | | `tool_version` | string | No | Optional tool version to use for tool execution | --- # DOCUMENT BOUNDARY --- # HarvestAPI > Connect to HarvestAPI to log time in Harvest and access LinkedIn data — profiles, companies, posts, ads, jobs, and connection management. Connect to HarvestAPI to log and retrieve time entries in Harvest, and access live LinkedIn data — scrape profiles, companies, and jobs; search for people, companies, and posts; and manage connection requests and messages on behalf of a LinkedIn account. ![HarvestAPI logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/harvest.svg) Supports authentication: API Key ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your HarvestAPI key with Scalekit so it can authenticate LinkedIn data requests on your behalf. You’ll need an API key from your HarvestAPI dashboard. Note HarvestAPI uses a pay-as-you-go credit model. Each scrape or search request consumes credits from your HarvestAPI account. Monitor your credit balance at [harvest-api.com/admin](https://harvest-api.com/admin) to avoid unexpected request failures. 1. ### Generate an API key * Sign in to your [HarvestAPI dashboard](https://harvest-api.com/admin/api-keys). * Click **Create API key**, give it a descriptive name (e.g., `My Agent`), and click **Create**. * Copy the generated API key. **It is shown only once** — store it securely before navigating away. ![](/.netlify/images?url=_astro%2Fcreate-api-key.BKixKj_W.png\&w=1366\&h=860\&dpl=69cce21a4f77360008b1503a) 2. ### Create a connection in Scalekit In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **HarvestAPI** and click **Create**. ![](/.netlify/images?url=_astro%2Fadd-credentials.BJf-mCLj.png\&w=1500\&h=520\&dpl=69cce21a4f77360008b1503a) 3. ### Add a connected account Open the connection you just created and click the **Connected Accounts** tab → **Add account**. Fill in the required fields: * **Your User’s ID** — a unique identifier for the user in your system * **API Key** — the key you copied in step 1 ![](/.netlify/images?url=_astro%2Fadd-connected-account.Ch5pQcte.png\&w=940\&h=504\&dpl=69cce21a4f77360008b1503a) Click **Save**. ## Usage [Section titled “Usage”](#usage) Once a connected account is set up, call LinkedIn data tools on behalf of any user — Scalekit injects the stored API key into every request automatically. You can interact with Harvest API in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'harvestapi'; // connection name from Scalekit dashboard 5 const identifier = 'user_123'; // must match the identifier used when adding the connected account 6 7 // Get credentials from app.scalekit.com → Developers → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Scrape a LinkedIn profile by URL 16 const profile = await actions.request({ 17 connectionName, 18 identifier, 19 path: '/linkedin/profile', 20 method: 'GET', 21 queryParams: { url: 'https://www.linkedin.com/in/satyanadella' }, 22 }); 23 console.log(profile.data); 24 25 // Search LinkedIn for people by title and location 26 const people = await actions.request({ 27 connectionName, 28 identifier, 29 path: '/linkedin/lead-search', 30 method: 'GET', 31 queryParams: { title: 'VP of Engineering', location: 'San Francisco, CA' }, 32 }); 33 console.log(people.data); 34 35 // Scrape a LinkedIn company page 36 const company = await actions.request({ 37 connectionName, 38 identifier, 39 path: '/linkedin/company', 40 method: 'GET', 41 queryParams: { url: 'https://www.linkedin.com/company/openai' }, 42 }); 43 console.log(company.data); 44 45 // Search LinkedIn job listings by keyword and location 46 const jobs = await actions.request({ 47 connectionName, 48 identifier, 49 path: '/linkedin/job-search', 50 method: 'GET', 51 queryParams: { keywords: 'machine learning engineer', location: 'New York, NY' }, 52 }); 53 console.log(jobs.data); 54 55 // Scrape a single job listing by URL 56 const job = await actions.request({ 57 connectionName, 58 identifier, 59 path: '/linkedin/job', 60 method: 'GET', 61 queryParams: { url: 'https://www.linkedin.com/jobs/view/1234567890' }, 62 }); 63 console.log(job.data); 64 65 // Bulk scrape multiple LinkedIn profiles in one request 66 const bulk = await actions.request({ 67 connectionName, 68 identifier, 69 path: '/v2/acts/harvestapi~linkedin-profile-scraper/run-sync-get-dataset-items', 70 method: 'POST', 71 body: { 72 urls: [ 73 'https://www.linkedin.com/in/satyanadella', 74 'https://www.linkedin.com/in/jeffweiner08', 75 'https://www.linkedin.com/in/reidhoffman', 76 ], 77 }, 78 }); 79 console.log(bulk.data); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "harvestapi" # connection name from Scalekit dashboard 6 identifier = "user_123" # must match the identifier used when adding the connected account 7 8 # Get credentials from app.scalekit.com → Developers → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 15 # Scrape a LinkedIn profile by URL 16 profile = scalekit_client.actions.request( 17 connection_name=connection_name, 18 identifier=identifier, 19 path="/linkedin/profile", 20 method="GET", 21 params={"url": "https://www.linkedin.com/in/satyanadella"} 22 ) 23 print(profile) 24 25 # Search LinkedIn for people by title and location 26 people = scalekit_client.actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/linkedin/lead-search", 30 method="GET", 31 params={"title": "VP of Engineering", "location": "San Francisco, CA"} 32 ) 33 print(people) 34 35 # Scrape a LinkedIn company page 36 company = scalekit_client.actions.request( 37 connection_name=connection_name, 38 identifier=identifier, 39 path="/linkedin/company", 40 method="GET", 41 params={"url": "https://www.linkedin.com/company/openai"} 42 ) 43 print(company) 44 45 # Search LinkedIn job listings by keyword and location 46 jobs = scalekit_client.actions.request( 47 connection_name=connection_name, 48 identifier=identifier, 49 path="/linkedin/job-search", 50 method="GET", 51 params={"keywords": "machine learning engineer", "location": "New York, NY"} 52 ) 53 print(jobs) 54 55 # Scrape a single job listing by URL 56 job = scalekit_client.actions.request( 57 connection_name=connection_name, 58 identifier=identifier, 59 path="/linkedin/job", 60 method="GET", 61 params={"url": "https://www.linkedin.com/jobs/view/1234567890"} 62 ) 63 print(job) 64 65 # Bulk scrape multiple LinkedIn profiles in one request 66 bulk = scalekit_client.actions.request( 67 connection_name=connection_name, 68 identifier=identifier, 69 path="/v2/acts/harvestapi~linkedin-profile-scraper/run-sync-get-dataset-items", 70 method="POST", 71 json={ 72 "urls": [ 73 "https://www.linkedin.com/in/satyanadella", 74 "https://www.linkedin.com/in/jeffweiner08", 75 "https://www.linkedin.com/in/reidhoffman" 76 ] 77 } 78 ) 79 print(bulk) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `log_time_entry` [Section titled “log\_time\_entry”](#log_time_entry) Create a new time entry in Harvest for a specific project and task. Returns the created time entry with its ID, billable status, and invoice details. Supports two logging modes: * **Duration-based**: provide `hours` (e.g., `1.5` for 90 minutes) * **Timer-based**: provide both `started_time` and `ended_time` (e.g., `8:00am` / `9:30am`) If neither `hours` nor start/end times are provided, Harvest creates a running timer that you must stop manually. | Name | Type | Required | Description | | -------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `project_id` | integer | Yes | Harvest project ID to log time against. Use `list_projects` to find IDs. | | `task_id` | integer | Yes | Harvest task ID within the project. | | `spent_date` | string | Yes | Date of the time entry in `YYYY-MM-DD` format (e.g., `2025-03-16`) | | `hours` | number | No | Duration in decimal hours (e.g., `1.5` for 90 minutes). Use this or `started_time`/`ended_time`. | | `started_time` | string | No | Start time for timer-based entry (e.g., `8:00am`). Must be paired with `ended_time`. | | `ended_time` | string | No | End time for timer-based entry (e.g., `9:30am`). Must be paired with `started_time`. | | `notes` | string | No | Notes or description for the time entry | | `user_id` | integer | No | User ID to log time for. Defaults to the authenticated user. Admin permissions required to log for another user. | ## `list_time_entries` [Section titled “list\_time\_entries”](#list_time_entries) List time entries in your Harvest account with optional filters by project, user, client, task, or date range. Returns paginated time entries with hours logged, notes, billable status, and associated project, task, and user details. | Name | Type | Required | Description | | ------------ | ------- | -------- | ---------------------------------------------------------- | | `project_id` | integer | No | Filter by Harvest project ID | | `user_id` | integer | No | Filter by Harvest user ID | | `client_id` | integer | No | Filter by Harvest client ID | | `task_id` | integer | No | Filter by Harvest task ID | | `from` | string | No | Start of date range in `YYYY-MM-DD` format | | `to` | string | No | End of date range in `YYYY-MM-DD` format | | `is_billed` | boolean | No | Filter by billed status | | `is_running` | boolean | No | Return only active running timers when `true` | | `page` | integer | No | Page number for pagination. Defaults to `1`. | | `per_page` | integer | No | Number of results per page (max `100`). Defaults to `100`. | ## `list_projects` [Section titled “list\_projects”](#list_projects) List all projects in your Harvest account. Returns project details including name, client, budget, billing method, start and end dates, and active status. Use this to find `project_id` values for `log_time_entry`. | Name | Type | Required | Description | | --------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------- | | `client_id` | integer | No | Filter projects by client ID | | `is_active` | boolean | No | Filter by active status. Set `true` to return only active projects. | | `updated_since` | string | No | ISO 8601 datetime — return only projects updated after this timestamp (e.g., `2025-01-01T00:00:00Z`) | | `page` | integer | No | Page number for pagination. Defaults to `1`. | | `per_page` | integer | No | Number of results per page (max `100`). Defaults to `100`. | ## `list_users` [Section titled “list\_users”](#list_users) List all users in your Harvest account. Returns user profiles including name, email, roles, and weekly capacity. Use this to find `user_id` values for filtering time entries or logging on behalf of another user. | Name | Type | Required | Description | | --------------- | ------- | -------- | ------------------------------------------------------------------ | | `is_active` | boolean | No | Filter by active status. Set `true` to return only active users. | | `updated_since` | string | No | ISO 8601 datetime — return only users updated after this timestamp | | `page` | integer | No | Page number for pagination. Defaults to `1`. | | `per_page` | integer | No | Number of results per page (max `100`). Defaults to `100`. | ## `get_user` [Section titled “get\_user”](#get_user) Retrieve a Harvest user profile by user ID, including name, email, roles, weekly capacity, and avatar. Use `list_users` to discover user IDs. | Name | Type | Required | Description | | --------- | ------ | -------- | --------------- | | `user_id` | string | Yes | Harvest user ID | ## `get_company` [Section titled “get\_company”](#get_company) Retrieve the Harvest company (account) information for the authenticated user, including company name, base URI, plan type, clock format, currency, and weekly capacity settings. Takes no parameters. ## `scrape_profile` [Section titled “scrape\_profile”](#scrape_profile) Scrape a LinkedIn profile by URL or public identifier. Returns contact details, current and past employment history, education, skills, and profile metadata. Provide either `profile_url` or `public_identifier` — at least one is required. Use `main=true` to get a simplified profile faster and at fewer credits. Enable `find_email=true` only when email is needed — it costs extra credits per successful match. | Name | Type | Required | Description | | ------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------- | | `profile_url` | string | No\* | Full LinkedIn profile URL (e.g., `https://www.linkedin.com/in/satyanadella`). Use this or `public_identifier`. | | `public_identifier` | string | No\* | LinkedIn profile handle — the slug after `/in/` (e.g., `satyanadella`). Use this or `profile_url`. | | `main` | boolean | No | Return a simplified profile at fewer credits (\~2.6s). Defaults to `false` (full profile, \~4.9s). | | `find_email` | boolean | No | Attempt to find the contact’s email address. Costs extra credits per successful match. Defaults to `false`. | \*Provide either `profile_url` or `public_identifier` — at least one is required. ## `scrape_company` [Section titled “scrape\_company”](#scrape_company) Scrape a LinkedIn company page for overview, headcount, employee count range, follower count, locations, specialties, industries, and funding data. Provide at least one of `company_url`, `universal_name`, or `search`. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------------------------------------------------------------------------ | | `company_url` | string | No\* | Full LinkedIn company page URL (e.g., `https://www.linkedin.com/company/microsoft`). | | `universal_name` | string | No\* | Company universal name — the slug after `/company/` (e.g., `microsoft`). | | `search` | string | No\* | Company name to search for. Returns the most relevant match. Useful when you don’t have the URL. | \*Provide at least one of `company_url`, `universal_name`, or `search`. ## `scrape_job` [Section titled “scrape\_job”](#scrape_job) Retrieve full job listing details from LinkedIn. Returns title, company, description, requirements, salary, location, workplace type, employment type, applicant count, and application details. Provide either `job_url` or `job_id` — at least one is required. | Name | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------ | | `job_url` | string | No\* | Full LinkedIn job posting URL (e.g., `https://www.linkedin.com/jobs/view/1234567890`). Use this or `job_id`. | | `job_id` | string | No\* | LinkedIn job listing ID. Use this or `job_url`. | \*Provide either `job_url` or `job_id` — at least one is required. ## `bulk_scrape_profiles` [Section titled “bulk\_scrape\_profiles”](#bulk_scrape_profiles) Batch scrape multiple LinkedIn profiles in a single request using the HarvestAPI Apify scraper. Returns an array of profile objects in the same order as the input. Failed profiles return an error object instead of profile data. **Pricing**: $4 per 1,000 profiles; $10 per 1,000 profiles with email. Note `bulk_scrape_profiles` requires a separate **Apify** account and API token. Obtain your token from [console.apify.com/settings/integrations](https://console.apify.com/settings/integrations). Apify usage is billed separately from your HarvestAPI credits. | Name | Type | Required | Description | | ------------- | --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `urls` | `array` | Yes | List of LinkedIn profile URLs to scrape. Each entry must be a full URL (e.g., `https://www.linkedin.com/in/username`). Maximum 50 URLs per request. | | `apify_token` | string | Yes | Apify API token from [console.apify.com/settings/integrations](https://console.apify.com/settings/integrations) | ## `search_profiles` [Section titled “search\_profiles”](#search_profiles) Search LinkedIn profiles using basic filters. Returns paginated profiles with name, title, location, and LinkedIn URL. Tip **`search_profiles` vs `search_people`**: Use `search_profiles` for quick name/title/company lookups. Use `search_people` when you need unmasked results via LinkedIn Lead Search, or when you need advanced filters like industry and comma-separated multi-value inputs. | Name | Type | Required | Description | | ---------- | ------- | -------- | ----------------------------------------------------- | | `search` | string | Yes | Name or keywords to search for (e.g., `"Jane Smith"`) | | `title` | string | No | Job title filter (e.g., `"Software Engineer"`) | | `company` | string | No | Current or past company name filter | | `school` | string | No | School or university name filter | | `location` | string | No | Location filter by city, state, or country | | `page` | integer | No | Page number for pagination. Defaults to `1`. | ## `search_people` [Section titled “search\_people”](#search_people) Search LinkedIn for people using filters such as job title, current company, location, and industry. Uses LinkedIn Lead Search for unmasked results. All parameters are optional and support comma-separated values for multiple filters. | Name | Type | Required | Description | | ---------- | ------- | -------- | ----------------------------------------------------------------------------------------- | | `keywords` | string | No | Free-text search terms matched against name, headline, and bio | | `title` | string | No | Job title filter (e.g., `"VP of Engineering"`). Comma-separate multiple values. | | `company` | string | No | Current company name filter (e.g., `"OpenAI,Anthropic"`). Comma-separate multiple values. | | `location` | string | No | Location filter by city, state, or country. Comma-separate multiple values. | | `industry` | string | No | Industry vertical filter (e.g., `"Software,Healthcare"`). Comma-separate multiple values. | | `page` | integer | No | Page number for pagination. Defaults to `1`. | ## `search_profile_services` [Section titled “search\_profile\_services”](#search_profile_services) Search LinkedIn profiles that offer specific services — freelancers, consultants, and service providers. Returns paginated profiles with name, position, location, and LinkedIn URL. | Name | Type | Required | Description | | ---------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------- | | `search` | string | Yes | Service name or keywords (e.g., `"UX design"`, `"tax consulting"`) | | `location` | string | No | Location filter by city, state, or country | | `geo_id` | string | No | LinkedIn Geo ID for the location — overrides `location` when provided. Use `search_geo_id` to look up Geo IDs. | | `page` | integer | No | Page number for pagination. Defaults to `1`. | ## `search_jobs` [Section titled “search\_jobs”](#search_jobs) Search LinkedIn job listings by keyword, location, company, workplace type, employment type, experience level, and salary. Returns paginated job listings with title, company, location, and LinkedIn URL. | Name | Type | Required | Description | | ------------------ | ------- | -------- | ----------------------------------------------------------------------------------------------------- | | `keywords` | string | No | Job title or skill keywords (e.g., `"machine learning engineer"`) | | `location` | string | No | Location filter by city, state, or country (e.g., `"New York, NY"`) | | `company` | string | No | Filter listings by company name (e.g., `"Stripe"`) | | `workplace_type` | string | No | Filter by workplace arrangement: `remote`, `on-site`, or `hybrid` | | `employment_type` | string | No | Filter by contract type: `full-time`, `part-time`, `contract`, `temporary`, `volunteer`, `internship` | | `experience_level` | string | No | Filter by seniority: `entry`, `associate`, `mid-senior`, `director`, `executive` | | `salary` | string | No | Salary range filter (format varies by region) | | `page` | integer | No | Page number for pagination. Defaults to `1`. | ## `search_companies` [Section titled “search\_companies”](#search_companies) Search for companies on LinkedIn by name, industry, company size, or location. Returns a paginated list of company results with name, domain, headcount, industry, and LinkedIn URL. | Name | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `search` | string | Yes | Keywords to match against company names (e.g., `"AI infrastructure"`) | | `location` | string | No | Filter by city, state, or country | | `company_size` | string | No | Filter by headcount range: `1-10`, `11-50`, `51-200`, `201-500`, `501-1000`, `1001-5000`, `5001-10000`, `10001+` | | `industry_id` | string | No | Filter by LinkedIn industry ID. Comma-separate multiple IDs (e.g., `"4,96"`). Industry IDs can be found in the LinkedIn API documentation. | | `page` | integer | No | Page number for pagination. Defaults to `1`. | ## `search_groups` [Section titled “search\_groups”](#search_groups) Search for LinkedIn groups by keyword or topic. Returns group name, member count, description, and LinkedIn URL. Use this to discover groups before fetching posts or details with `get_linkedin_group` and `get_group_posts`. | Name | Type | Required | Description | | -------- | ------- | -------- | ------------------------------------------------------------- | | `search` | string | Yes | Keywords to search for groups (e.g., `"AI product managers"`) | | `page` | integer | No | Page number for pagination. Defaults to `1`. | ## `get_linkedin_group` [Section titled “get\_linkedin\_group”](#get_linkedin_group) Retrieve details for a LinkedIn group including name, description, member count, and group metadata. Provide either `group_url` or `group_id` — at least one is required. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------------------------------------------------------------- | | `group_url` | string | No\* | Full LinkedIn group URL (e.g., `https://www.linkedin.com/groups/1234567`). Use this or `group_id`. | | `group_id` | string | No\* | LinkedIn group ID. Use this or `group_url`. | \*Provide either `group_url` or `group_id` — at least one is required. ## `get_group_posts` [Section titled “get\_group\_posts”](#get_group_posts) Retrieve posts from a LinkedIn group. Returns post content, author info, engagement stats, and timestamps. Provide either `group_url` or `group_id`. | Name | Type | Required | Description | | ----------- | ------- | -------- | ------------------------------------------------ | | `group_url` | string | No\* | Full LinkedIn group URL. Use this or `group_id`. | | `group_id` | string | No\* | LinkedIn group ID. Use this or `group_url`. | | `page` | integer | No | Page number for pagination. Defaults to `1`. | \*Provide either `group_url` or `group_id` — at least one is required. ## `get_profile_posts` [Section titled “get\_profile\_posts”](#get_profile_posts) Retrieve recent LinkedIn posts from a person’s profile. Returns post content, engagement stats, and timestamps. Provide either `profile_url` or `public_identifier`. | Name | Type | Required | Description | | ------------------- | ------- | -------- | ------------------------------------------------------------------------------------------ | | `profile_url` | string | No\* | Full LinkedIn profile URL. Use this or `public_identifier`. | | `public_identifier` | string | No\* | LinkedIn handle — the slug after `/in/` (e.g., `satyanadella`). Use this or `profile_url`. | | `page` | integer | No | Page number for pagination. Defaults to `1`. | \*Provide either `profile_url` or `public_identifier` — at least one is required. ## `get_company_posts` [Section titled “get\_company\_posts”](#get_company_posts) Retrieve recent LinkedIn posts from a company page. Returns post content, likes, comments, shares, and engagement stats. Provide either `company_url` or `universal_name`. | Name | Type | Required | Description | | ---------------- | ------- | -------- | ------------------------------------------------------------------------------------------------ | | `company_url` | string | No\* | Full LinkedIn company page URL. Use this or `universal_name`. | | `universal_name` | string | No\* | Company universal name — the slug after `/company/` (e.g., `openai`). Use this or `company_url`. | | `posted_limit` | string | No | Filter posts by recency: `24h`, `week`, or `month` | | `page` | integer | No | Page number for pagination. Defaults to `1`. | \*Provide either `company_url` or `universal_name` — at least one is required. ## `search_posts` [Section titled “search\_posts”](#search_posts) Search for LinkedIn posts by keyword, author, company, or content type. Returns paginated posts with content, engagement metrics, and author info. All parameters are optional — provide at least one for meaningful results. | Name | Type | Required | Description | | -------------- | ------- | -------- | -------------------------------------------------------------------------- | | `search` | string | No | Keywords to search in post content (e.g., `"generative AI regulation"`) | | `profile` | string | No | Filter posts by author LinkedIn profile URL. Comma-separate multiple URLs. | | `company` | string | No | Filter posts by company LinkedIn URL. Comma-separate multiple URLs. | | `content_type` | string | No | Filter by post format: `videos`, `images`, `documents`, `live_videos` | | `posted_limit` | string | No | Filter by recency: `24h`, `week`, or `month` | | `sort_by` | string | No | Sort order: `relevance` (default) or `date` | | `page` | integer | No | Page number for pagination. Defaults to `1`. | ## `get_linkedin_post` [Section titled “get\_linkedin\_post”](#get_linkedin_post) Retrieve full details of a LinkedIn post, including content, author, likes, comments, shares, and engagement metrics. Provide either `post_url` or `activity_id` — at least one is required. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------------------------------------------------- | | `post_url` | string | No\* | Full LinkedIn post URL. Use this or `activity_id`. | | `activity_id` | string | No\* | LinkedIn activity ID of the post (found in the post URL). Use this or `post_url`. | \*Provide either `post_url` or `activity_id` — at least one is required. ## `get_post_comments` [Section titled “get\_post\_comments”](#get_post_comments) Retrieve comments on a LinkedIn post. Returns commenter profiles, comment text, likes, and timestamps. | Name | Type | Required | Description | | --------- | ------- | -------- | -------------------------------------------- | | `post` | string | Yes | Full LinkedIn post URL | | `sort_by` | string | No | Sort order: `relevance` (default) or `date` | | `page` | integer | No | Page number for pagination. Defaults to `1`. | ## `get_post_reactions` [Section titled “get\_post\_reactions”](#get_post_reactions) Retrieve users who reacted to a LinkedIn post, including their name, title, and reaction type. **Reaction types**: `like`, `celebrate`, `support`, `insightful`, `funny`, `love` | Name | Type | Required | Description | | ------ | ------- | -------- | -------------------------------------------- | | `post` | string | Yes | Full LinkedIn post URL | | `page` | integer | No | Page number for pagination. Defaults to `1`. | ## `get_comment_replies` [Section titled “get\_comment\_replies”](#get_comment_replies) Retrieve replies to a specific LinkedIn comment. Returns reply content, author info, and engagement stats. | Name | Type | Required | Description | | ------------- | ------- | -------- | -------------------------------------------- | | `comment_url` | string | Yes | LinkedIn comment URL or comment ID | | `page` | integer | No | Page number for pagination. Defaults to `1`. | ## `get_comment_reactions` [Section titled “get\_comment\_reactions”](#get_comment_reactions) Retrieve users who reacted to a specific LinkedIn comment, including their name, title, and reaction type. | Name | Type | Required | Description | | ------------- | ------- | -------- | -------------------------------------------- | | `comment_url` | string | Yes | LinkedIn comment URL or comment ID | | `page` | integer | No | Page number for pagination. Defaults to `1`. | ## `get_profile_comments` [Section titled “get\_profile\_comments”](#get_profile_comments) Retrieve recent comments made by a LinkedIn profile. Returns comment content, the post context, and timestamps. Provide either `profile_url` or `public_identifier`. | Name | Type | Required | Description | | ------------------- | ------- | -------- | ------------------------------------------------------------------- | | `profile_url` | string | No\* | Full LinkedIn profile URL. Use this or `public_identifier`. | | `public_identifier` | string | No\* | LinkedIn handle — the slug after `/in/`. Use this or `profile_url`. | | `page` | integer | No | Page number for pagination. Defaults to `1`. | \*Provide either `profile_url` or `public_identifier` — at least one is required. ## `get_profile_reactions` [Section titled “get\_profile\_reactions”](#get_profile_reactions) Retrieve recent reactions (likes, celebrates, etc.) made by a LinkedIn profile on posts and articles. Provide either `profile_url` or `public_identifier`. | Name | Type | Required | Description | | ------------------- | ------- | -------- | ------------------------------------------------------------------- | | `profile_url` | string | No\* | Full LinkedIn profile URL. Use this or `public_identifier`. | | `public_identifier` | string | No\* | LinkedIn handle — the slug after `/in/`. Use this or `profile_url`. | | `page` | integer | No | Page number for pagination. Defaults to `1`. | \*Provide either `profile_url` or `public_identifier` — at least one is required. ## `search_linkedin_ads` [Section titled “search\_linkedin\_ads”](#search_linkedin_ads) Search LinkedIn ads using keywords, account owner, country, and date filters. Returns paginated ad results including ad content, creative, and targeting details. Provide either `search_url` (a LinkedIn Ad Library URL) or keyword/filter parameters — the two approaches are mutually exclusive. | Name | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------------------------------------------------------------------------------------- | | `search_url` | string | No | LinkedIn Ad Library search URL. Use this or keyword/filter parameters. | | `keyword` | string | No | Search ads by keyword | | `account_owner` | string | No | Filter by company or advertiser name | | `countries` | string | No | Comma-separated ISO country codes (e.g., `"US,GB"`) or `"ALL"` for global results | | `date_option` | string | No | Date range preset: `last-30-days`, `current-month`, `current-year`, `last-year`, or `custom-date-range` | | `start_date` | string | No | Start of custom date range in `YYYY-MM-DD` format. Only applies when `date_option` is `custom-date-range`. | | `end_date` | string | No | End of custom date range in `YYYY-MM-DD` format. Only applies when `date_option` is `custom-date-range`. | | `pagination_token` | string | No | Token returned in the previous response for fetching the next page of results | ## `get_linkedin_ad_details` [Section titled “get\_linkedin\_ad\_details”](#get_linkedin_ad_details) Retrieve details of a specific LinkedIn ad from the Ad Library, including ad content, creative, advertiser, and targeting information. Provide either `ad_url` or `ad_id` — at least one is required. | Name | Type | Required | Description | | -------- | ------ | -------- | ----------------------------------------------------------------- | | `ad_url` | string | No\* | LinkedIn Ad Library URL for the specific ad. Use this or `ad_id`. | | `ad_id` | string | No\* | LinkedIn ad ID. Use this or `ad_url`. | \*Provide either `ad_url` or `ad_id` — at least one is required. ## `search_geo_id` [Section titled “search\_geo\_id”](#search_geo_id) Search for LinkedIn geographic IDs (`geoId`) by location name. Use the returned `geoId` values as precise location filters in `search_jobs` and `search_profile_services` — `geoId` overrides text-based `location` filters and produces more accurate results. | Name | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------------------------------------------------ | | `search` | string | Yes | Location name to look up (e.g., `"San Francisco"`, `"United Kingdom"`, `"Greater London"`) | ## Connection management tools [Section titled “Connection management tools”](#connection-management-tools) Caution The following tools — `send_connection_request`, `get_sent_connection_requests`, `get_received_connection_requests`, `accept_connection_request`, and `send_linkedin_message` — act on behalf of a specific LinkedIn account and require the account’s **LinkedIn session cookie** (`li_at`), not the HarvestAPI API key. **How to find your `li_at` cookie:** 1. Log in to [linkedin.com](https://www.linkedin.com) in your browser. 2. Open browser developer tools (F12) → **Application** tab → **Cookies** → `https://www.linkedin.com`. 3. Find the cookie named `li_at` and copy its value. Store `li_at` securely. It expires when the LinkedIn session ends or the user logs out. ## `send_connection_request` [Section titled “send\_connection\_request”](#send_connection_request) Send a LinkedIn connection request to a profile on behalf of a LinkedIn account. Optionally include a personalized message (max 300 characters). | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------------------------------------------------- | | `profile` | string | Yes | LinkedIn profile URL, public identifier, or profile ID of the recipient | | `cookie` | string | Yes | LinkedIn session cookie (`li_at`) for the sending account | | `message` | string | No | Personalized connection message (max 300 characters). Omit to send without a note. | | `proxy` | string | No | Proxy server URL for the request (e.g., `http://user:pass@proxy.example.com:8080`) | ## `get_sent_connection_requests` [Section titled “get\_sent\_connection\_requests”](#get_sent_connection_requests) Retrieve pending connection requests sent from a LinkedIn account. Returns recipient profiles and request timestamps. | Name | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------- | | `cookie` | string | Yes | LinkedIn session cookie (`li_at`) for the account | | `proxy` | string | No | Proxy server URL for the request | ## `get_received_connection_requests` [Section titled “get\_received\_connection\_requests”](#get_received_connection_requests) Retrieve pending connection requests received on a LinkedIn account. Returns sender profiles, optional messages, and request timestamps. Use the `invitation_id` and `shared_secret` from each result to accept requests with `accept_connection_request`. | Name | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------- | | `cookie` | string | Yes | LinkedIn session cookie (`li_at`) for the account | | `proxy` | string | No | Proxy server URL for the request | ## `accept_connection_request` [Section titled “accept\_connection\_request”](#accept_connection_request) Accept a pending LinkedIn connection request. The `invitation_id` and `shared_secret` are returned by `get_received_connection_requests`. | Name | Type | Required | Description | | --------------- | ------ | -------- | ----------------------------------------------------------------------------------- | | `invitation_id` | string | Yes | ID of the connection invitation to accept (from `get_received_connection_requests`) | | `shared_secret` | string | Yes | Shared secret for the invitation (from `get_received_connection_requests`) | | `cookie` | string | Yes | LinkedIn session cookie (`li_at`) for the account | | `proxy` | string | No | Proxy server URL for the request | ## `send_linkedin_message` [Section titled “send\_linkedin\_message”](#send_linkedin_message) Send a direct message to a LinkedIn connection on behalf of a LinkedIn account. The recipient must be a 1st-degree connection — you cannot message people you are not connected with. | Name | Type | Required | Description | | ------------------- | ------ | -------- | --------------------------------------------------------- | | `recipient_profile` | string | Yes | LinkedIn profile URL of the message recipient | | `message` | string | Yes | Text content of the direct message | | `cookie` | string | Yes | LinkedIn session cookie (`li_at`) for the sending account | | `proxy` | string | No | Proxy server URL for the request | ## `get_my_api_user` [Section titled “get\_my\_api\_user”](#get_my_api_user) Retrieve information about the current HarvestAPI user account. Call this before high-volume scraping workflows to verify you have sufficient credits. Takes no parameters. **Returns:** | Field | Description | | -------------- | --------------------------------------------- | | `id` | Your HarvestAPI user ID | | `email` | Account email address | | `credits` | Current credit balance | | `credits_used` | Total credits consumed to date | | `plan` | Active subscription plan name | | `rate_limit` | Maximum API requests per second for your plan | | `created_at` | Account creation timestamp | ## `get_private_account_pools` [Section titled “get\_private\_account\_pools”](#get_private_account_pools) Retrieve the list of private LinkedIn account pools configured on your HarvestAPI account. Private pools route requests through dedicated LinkedIn accounts, providing better rate limit isolation for high-volume workflows. Takes no parameters. **Returns an array of pools, each containing:** | Field | Description | | ---------------- | ---------------------------------------------------------- | | `id` | Pool ID — pass this as `usePrivatePool` in supported tools | | `name` | Display name of the pool | | `accounts_count` | Number of LinkedIn accounts in the pool | | `status` | Pool status: `active` or `inactive` | | `created_at` | Pool creation timestamp | --- # DOCUMENT BOUNDARY --- # HubSpot > Connect to HubSpot CRM. Manage contacts, deals, companies, and marketing automation Connect to HubSpot CRM. Manage contacts, deals, companies, and marketing automation ![HubSpot logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/hub_spot.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the HubSpot connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **HubSpot** and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.BKPumAWy.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Log in to your [HubSpot developer dashboard](https://developers.hubspot.com/), click **Manage apps**, and open your app. * Go to **Auth → Auth settings → Redirect URL**, paste the redirect URI, and click **Save**. ![Adding redirect URL to HubSpot](/.netlify/images?url=_astro%2Fadd-redirect-url.DZL9XRD7.png\&w=1216\&h=880\&dpl=69cce21a4f77360008b1503a) * Select the required scopes for your application under **Auth → Auth settings → Scopes**. 2. ### Get client credentials * In your HubSpot App, go to **Auth → Auth settings**. * Copy your **Client ID** and **Client Secret**. 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from above) * Client Secret (from above) * Permissions (scopes — see [HubSpot API Scopes reference](https://developers.hubspot.com/docs/api/overview)) Caution The scopes selected in your HubSpot app must exactly match the scopes you enter here. A mismatch will break the OAuth flow. ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.HJl-c2GR.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s HubSpot account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with HubSpot in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'hubspot'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize HubSpot:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/crm/v3/owners', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "hubspot" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize HubSpot:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/crm/v3/owners", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `hubspot_companies_search` [Section titled “hubspot\_companies\_search”](#hubspot_companies_search) Search HubSpot companies using full-text search and pagination. Returns matching companies with specified properties. | Name | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------------------------------------ | | `after` | string | No | Pagination offset to get results starting from a specific position | | `filterGroups` | string | No | JSON string containing filter groups for advanced filtering | | `limit` | number | No | Number of results to return per page (max 100) | | `properties` | string | No | Comma-separated list of properties to include in the response | | `query` | string | No | Search term for full-text search across company properties | ## `hubspot_company_create` [Section titled “hubspot\_company\_create”](#hubspot_company_create) Create a new company in HubSpot CRM. Requires a company name as the unique identifier. Supports additional properties like domain, industry, phone, location, and revenue information. | Name | Type | Required | Description | | ------------------- | ------ | -------- | ----------------------------------------------------- | | `annualrevenue` | number | No | Annual revenue of the company | | `city` | string | No | Company city location | | `country` | string | No | Company country location | | `description` | string | No | Company description or overview | | `domain` | string | No | Company website domain | | `industry` | string | No | Industry type of the company | | `name` | string | Yes | Company name (required, serves as primary identifier) | | `numberofemployees` | number | No | Number of employees at the company | | `phone` | string | No | Company phone number | | `state` | string | No | Company state or region | ## `hubspot_company_get` [Section titled “hubspot\_company\_get”](#hubspot_company_get) Retrieve details of a specific company from HubSpot by company ID. Returns company properties and associated data. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------- | | `company_id` | string | Yes | ID of the company to retrieve | | `properties` | string | No | Comma-separated list of properties to include in the response | ## `hubspot_contact_create` [Section titled “hubspot\_contact\_create”](#hubspot_contact_create) Create a new contact in HubSpot CRM. Requires an email address as the unique identifier. Supports additional properties like name, company, phone, and lifecycle stage. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------------------------------------------------------------------------- | | `company` | string | No | Company name where the contact works | | `email` | string | Yes | Primary email address for the contact (required, serves as unique identifier) | | `firstname` | string | No | First name of the contact | | `hs_lead_status` | string | No | Lead status of the contact | | `jobtitle` | string | No | Job title of the contact | | `lastname` | string | No | Last name of the contact | | `lifecyclestage` | string | No | Lifecycle stage of the contact | | `phone` | string | No | Phone number of the contact | | `website` | string | No | Personal or company website URL | ## `hubspot_contact_get` [Section titled “hubspot\_contact\_get”](#hubspot_contact_get) Retrieve details of a specific contact from HubSpot by contact ID. Returns contact properties and associated data. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------------------- | | `contact_id` | string | Yes | ID of the contact to retrieve | | `properties` | string | No | Comma-separated list of properties to include in the response | ## `hubspot_contact_update` [Section titled “hubspot\_contact\_update”](#hubspot_contact_update) Update an existing contact in HubSpot CRM by contact ID. Allows updating contact properties like name, email, company, phone, and lifecycle stage. | Name | Type | Required | Description | | ------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `contact_id` | string | Yes | ID of the contact to update | | `props` | `object` | No | Object containing properties like first name, last name, email, company, phone, and job title to update all these should be provided inside props as a JSON object, this is required | ## `hubspot_contacts_list` [Section titled “hubspot\_contacts\_list”](#hubspot_contacts_list) Retrieve a list of contacts from HubSpot with filtering and pagination. Returns contact properties and supports pagination through cursor-based navigation. | Name | Type | Required | Description | | ------------ | ------- | -------- | ------------------------------------------------------------- | | `after` | string | No | Pagination cursor to get the next set of results | | `archived` | boolean | No | Whether to include archived contacts in the results | | `limit` | number | No | Number of results to return per page (max 100) | | `properties` | string | No | Comma-separated list of properties to include in the response | ## `hubspot_contacts_search` [Section titled “hubspot\_contacts\_search”](#hubspot_contacts_search) Search HubSpot contacts using full-text search and pagination. Returns matching contacts with specified properties. | Name | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------------------------------------ | | `after` | string | No | Pagination offset to get results starting from a specific position | | `filterGroups` | string | No | JSON string containing filter groups for advanced filtering | | `limit` | number | No | Number of results to return per page (max 100) | | `properties` | string | No | Comma-separated list of properties to include in the response | | `query` | string | No | Search term for full-text search across contact properties | ## `hubspot_deal_create` [Section titled “hubspot\_deal\_create”](#hubspot_deal_create) Create a new deal in HubSpot CRM. Requires dealname, amount, and dealstage. Supports additional properties like pipeline, close date, and deal type. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------- | | `amount` | number | Yes | Deal amount/value (required) | | `closedate` | string | No | Expected close date (YYYY-MM-DD format) | | `dealname` | string | Yes | Name of the deal (required) | | `dealstage` | string | Yes | Current stage of the deal (required) | | `dealtype` | string | No | Type of deal | | `description` | string | No | Deal description | | `hs_priority` | string | No | Deal priority (HIGH, MEDIUM, LOW) | | `pipeline` | string | No | Deal pipeline | ## `hubspot_deal_update` [Section titled “hubspot\_deal\_update”](#hubspot_deal_update) Update an existing deal in HubSpot CRM by deal ID. Allows updating deal properties like name, amount, stage, pipeline, close date, and priority. | Name | Type | Required | Description | | ------------ | -------- | -------- | ---------------------------------------------- | | `deal_id` | string | Yes | ID of the deal to update | | `good_deal` | boolean | No | Boolean flag indicating if this is a good deal | | `properties` | `object` | Yes | Object containing deal properties to update | ## `hubspot_deals_search` [Section titled “hubspot\_deals\_search”](#hubspot_deals_search) Search HubSpot deals using full-text search and pagination. Returns matching deals with specified properties. | Name | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------------------------------------ | | `after` | string | No | Pagination offset to get results starting from a specific position | | `filterGroups` | string | No | JSON string containing filter groups for advanced filtering | | `limit` | number | No | Number of results to return per page (max 100) | | `properties` | string | No | Comma-separated list of properties to include in the response | | `query` | string | No | Search term for full-text search across deal properties | --- # DOCUMENT BOUNDARY --- # Intercom > Connect to Intercom. Send messages, manage conversations, and interact with users and contacts. Connect to Intercom. Send messages, manage conversations, and interact with users and contacts. ![Intercom logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/intercom.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Intercom connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. You’ll need your app credentials from the [Intercom Developer Hub](https://developers.intercom.com/). 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. * Find **Intercom** from the list of providers and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.D2jW9UeB.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * In the [Intercom Developer Hub](https://developers.intercom.com/), open your app and go to **Configure** → **Authentication**. * Click **Edit**, paste the copied URI into the **Redirect URLs** field, and click **Save**. ![Add redirect URL in Intercom Developer Hub](/.netlify/images?url=_astro%2Fadd-redirect-uri.Cy6-d1KD.png\&w=1440\&h=780\&dpl=69cce21a4f77360008b1503a) 2. ### Get client credentials * In the [Intercom Developer Hub](https://developers.intercom.com/), open your app and go to **Configure** → **Basic Information**: * **Client ID** — listed under **Client ID** * **Client Secret** — listed under **Client Secret** 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from your Intercom app) * Client Secret (from your Intercom app) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Intercom account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Intercom in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'intercom'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Intercom:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "intercom" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Intercom:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Jira > Connect to Jira. Manage issues, projects, workflows, and agile development processes Connect to Jira. Manage issues, projects, workflows, and agile development processes ![Jira logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/jira.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Jira connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Jira** and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.B82T4vOr.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * In the [Atlassian Developer Console](https://developer.atlassian.com/apps/), open your app and go to **Authorization** → **OAuth 2.0 (3LO)** → **Configure**. * Paste the copied URI into the **Callback URL** field and click **Save changes**. ![Add callback URL in Atlassian Developer Console for Jira](/.netlify/images?url=_astro%2Fadd-redirect-uri.D5X34MVH.gif\&w=1760\&h=608\&dpl=69cce21a4f77360008b1503a) 2. ### Get client credentials In the [Atlassian Developer Console](https://developer.atlassian.com/apps/), open your app and go to **Settings**: * **Client ID** — listed under **Client ID** * **Client Secret** — listed under **Secret** 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from your Atlassian app) * Client Secret (from your Atlassian app) * Permissions (scopes — see [Jira OAuth scopes reference](https://developer.atlassian.com/cloud/jira/platform/scopes-for-oauth-2-3LO-and-forge-apps/)) ![Add credentials for Jira in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Jira account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. **Don’t worry about the Jira cloud ID in the path.** Scalekit automatically resolves `{{cloud_id}}` from the connected account’s configuration. For example, a request with `path="/rest/api/3/myself"` will be sent to `https://api.atlassian.com/ex/jira/a1b2c3d4-e5f6-7890-abcd-ef1234567890/rest/api/3/myself` automatically. You can interact with Jira in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'jira'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Jira:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/rest/api/3/myself', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "jira" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Jira:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/rest/api/3/myself", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Linear > Connect to Linear. Manage issues, projects, sprints, and development workflows Connect to Linear. Manage issues, projects, sprints, and development workflows ![Linear logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/linear.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Linear connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Linear** and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.3i62OVNe.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Log in to [Linear](https://linear.app) and go to **Settings** → **API** → **OAuth applications**. * Click **New application**, enter an application name and description, then paste the redirect URI from Scalekit into the **Callback URLs** field. Click **Create application**. ![Create OAuth application in Linear](/.netlify/images?url=_astro%2Fadd-redirect-uri.CvKmaUzv.png\&w=1440\&h=900\&dpl=69cce21a4f77360008b1503a) 2. ### Get client credentials * In your Linear OAuth application, copy the **Client ID** and **Client Secret**. 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from above) * Client Secret (from above) * Permissions (scopes — see [Linear OAuth scopes reference](https://developers.linear.app/docs/oauth/authentication#oauth-2.0-scopes)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.HJl-c2GR.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Linear account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Linear in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'linear'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Linear:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a GraphQL request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/graphql', 29 method: 'POST', 30 body: JSON.stringify({ query: '{ viewer { id name email } }' }), 31 }); 32 console.log(result); ``` * Python ```python 1 import scalekit.client, os, json 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "linear" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Linear:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a GraphQL request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/graphql", 30 method="POST", 31 body=json.dumps({"query": "{ viewer { id name email } }"}) 32 ) 33 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `linear_graphql_query` [Section titled “linear\_graphql\_query”](#linear_graphql_query) Execute a custom GraphQL query or mutation against the Linear API. Allows running any valid GraphQL operation with variables support for advanced use cases. | Name | Type | Required | Description | | ----------- | -------- | -------- | ---------------------------------------- | | `query` | string | Yes | The GraphQL query or mutation to execute | | `variables` | `object` | No | Variables to pass to the GraphQL query | ## `linear_issue_create` [Section titled “linear\_issue\_create”](#linear_issue_create) Create a new issue in Linear using the issueCreate mutation. Requires a team ID and title at minimum. | Name | Type | Required | Description | | ------------- | --------------- | -------- | ---------------------------------------------------- | | `assigneeId` | string | No | ID of the user to assign the issue to | | `description` | string | No | Description of the issue | | `estimate` | string | No | Story point estimate for the issue | | `labelIds` | `array` | No | Array of label IDs to apply to the issue | | `priority` | string | No | Priority level of the issue (1-4, where 1 is urgent) | | `projectId` | string | No | ID of the project to associate the issue with | | `stateId` | string | No | ID of the workflow state to set | | `teamId` | string | Yes | ID of the team to create the issue in | | `title` | string | Yes | Title of the issue | ## `linear_issue_update` [Section titled “linear\_issue\_update”](#linear_issue_update) Update an existing issue in Linear. You can update title, description, priority, state, and assignee. | Name | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------------------------------- | | `assigneeId` | string | No | ID of the user to assign the issue to | | `description` | string | No | New description for the issue | | `issueId` | string | Yes | ID of the issue to update | | `priority` | string | No | Priority level of the issue (1-4, where 1 is urgent) | | `stateId` | string | No | ID of the workflow state to set | | `title` | string | No | New title for the issue | ## `linear_issues_list` [Section titled “linear\_issues\_list”](#linear_issues_list) List issues in Linear using the issues query with simple filtering and pagination support. | Name | Type | Required | Description | | ---------- | --------------- | -------- | ------------------------------------------------------------ | | `after` | string | No | Cursor for pagination (returns issues after this cursor) | | `assignee` | string | No | Filter by assignee email (e.g., ‘’) | | `before` | string | No | Cursor for pagination (returns issues before this cursor) | | `first` | integer | No | Number of issues to return (pagination) | | `labels` | `array` | No | Filter by label names (array of strings) | | `priority` | string | No | Filter by priority level (1=Urgent, 2=High, 3=Medium, 4=Low) | | `project` | string | No | Filter by project name (e.g., ‘Q4 Goals’) | | `state` | string | No | Filter by state name (e.g., ‘In Progress’, ‘Done’) | --- # DOCUMENT BOUNDARY --- # Microsoft Excel > Connect to Microsoft Excel. Access, read, and modify spreadsheets stored in OneDrive or SharePoint through Microsoft Graph API. Connect to Microsoft Excel. Access, read, and modify spreadsheets stored in OneDrive or SharePoint through Microsoft Graph API. ![Microsoft Excel logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/excel.svg) Supports authentication: OAuth 2.0 ## Usage [Section titled “Usage”](#usage) Connect a user’s Microsoft Excel account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'microsoftexcel'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Microsoft Excel:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1.0/me/drive/root/children', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "microsoftexcel" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Microsoft Excel:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1.0/me/drive/root/children", 30 method="GET" 31 ) 32 print(result) ``` --- # DOCUMENT BOUNDARY --- # Teams > Connect to Microsoft Teams. Manage messages, channels, meetings, and team collaboration Connect to Microsoft Teams. Manage messages, channels, meetings, and team collaboration ![Teams logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/microsoft-teams.svg) Supports authentication: OAuth 2.0 ## Usage [Section titled “Usage”](#usage) Connect a user’s Microsoft Teams account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'microsoftteams'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Microsoft Teams:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1.0/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "microsoftteams" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Microsoft Teams:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1.0/me", 30 method="GET" 31 ) 32 print(result) ``` --- # DOCUMENT BOUNDARY --- # Microsoft Word > Connect to Microsoft Word. Authenticate with your Microsoft account to create, read, and edit Word documents stored in OneDrive or SharePoint through Microsoft Graph API. Connect to Microsoft Word. Authenticate with your Microsoft account to create, read, and edit Word documents stored in OneDrive or SharePoint through Microsoft Graph API. ![Microsoft Word logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/word.svg) Supports authentication: OAuth 2.0 ## Usage [Section titled “Usage”](#usage) Connect a user’s Microsoft Word account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Microsoft Word in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'microsoftword'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Microsoft Word:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1.0/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "microsoftword" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Microsoft Word:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1.0/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Monday.com > Connect to Monday.com. Manage boards, tasks, workflows, teams, and project collaboration Connect to Monday.com. Manage boards, tasks, workflows, teams, and project collaboration ![Monday.com logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/monday.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Monday.com connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. You’ll need your app credentials from the [Monday.com Developer Center](https://monday.com/developers/apps). 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. * Find **Monday.com** from the list of providers and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.CjHVkKig.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * In the [Monday.com Developer Center](https://monday.com/developers/apps), open your app and go to the **OAuth** tab. * Add the copied URI under **Redirect URLs** and save. ![Add redirect URL in Monday.com Developer Center](/.netlify/images?url=_astro%2Fadd-redirect-uri.DChkuXdv.png\&w=1440\&h=780\&dpl=69cce21a4f77360008b1503a) 2. ### Get client credentials * In the [Monday.com Developer Center](https://monday.com/developers/apps), open your app and go to the **Basic Information** tab: * **Client ID** — listed under **Client ID** * **Client Secret** — listed under **Client Secret** 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from your Monday.com app) * Client Secret (from your Monday.com app) * Permissions — select the scopes your app needs (see [Monday.com OAuth scopes](https://developer.monday.com/apps/docs/oauth-scopes)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Monday.com account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Monday in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'monday'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Monday.com:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v2', 29 method: 'POST', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "monday" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Monday.com:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v2", 30 method="POST" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Notion > Connect to Notion workspace. Create, edit pages, manage databases, and collaborate on content Connect to Notion workspace. Create, edit pages, manage databases, and collaborate on content ![Notion logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/notion.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Notion connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Notion** and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.DBrgMIG1.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Go to [Notion Integrations](https://www.notion.so/profile/integrations) and click **New integration**. * Fill in the integration name and select your workspace. In the **OAuth Domain & URIs** section, paste the redirect URI from Scalekit and click **Submit**. ![Add redirect URI in Notion integration settings](/.netlify/images?url=_astro%2Fadd-redirect-uri.DIG9xOG3.png\&w=1100\&h=560\&dpl=69cce21a4f77360008b1503a) 2. ### Get client credentials * In your Notion integration settings, go to the **Secrets** tab. * Copy the **OAuth client ID** and **OAuth client secret**. 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (OAuth client ID from above) * Client Secret (OAuth client secret from above) * Permissions (capabilities — see [Notion capabilities reference](https://developers.notion.com/reference/capabilities)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.B384Pfpy.png\&w=1392\&h=768\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Notion account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Notion in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'notion'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Notion:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1/users/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "notion" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Notion:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1/users/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `notion_block_delete` [Section titled “notion\_block\_delete”](#notion_block_delete) Delete (archive) a Notion block by its ID. This also deletes all child blocks within it. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------- | | `block_id` | string | Yes | The ID of the block to delete | ## `notion_block_update` [Section titled “notion\_block\_update”](#notion_block_update) Update the text content of an existing Notion block. Supports paragraph, heading, list item, quote, callout, and code blocks. | Name | Type | Required | Description | | ---------- | ------ | -------- | --------------------------------------------------- | | `block_id` | string | Yes | The ID of the block to update | | `language` | string | No | Programming language for code blocks | | `text` | string | Yes | New text content for the block | | `type` | string | Yes | The block type (must match the existing block type) | ## `notion_comment_create` [Section titled “notion\_comment\_create”](#notion_comment_create) Create a comment in Notion. Provide a comment object with rich\_text content and either a parent object (with page\_id) for a page-level comment or a discussion\_id to reply in an existing thread. | Name | Type | Required | Description | | ---------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------- | | `comment` | `object` | Yes | Comment object containing a rich\_text array. Example: `{"rich_text":[{"type":"text","text":{"content":"Hello"}}]}` | | `discussion_id` | string | No | Existing discussion thread ID to reply to. | | `notion_version` | string | No | Optional override for the Notion-Version header (e.g., 2022-06-28). | | `parent` | `object` | No | Parent object for a new top-level comment. Shape: `{"page_id":""}`. | | `schema_version` | string | No | Internal override for schema version. | | `tool_version` | string | No | Internal override for tool implementation version. | ## `notion_comment_retrieve` [Section titled “notion\_comment\_retrieve”](#notion_comment_retrieve) Retrieve a single Notion comment by its `comment_id`. LLM tip: you typically obtain `comment_id` from the response of creating a comment or by first listing comments for a page/block and selecting the desired item’s `id`. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `comment_id` | string | Yes | The identifier of the comment to retrieve (hyphenated UUID). Obtain it from Create-Comment responses or from a prior List-Comments call. | | `notion_version` | string | No | Optional Notion-Version header override (e.g., 2022-06-28). | | `schema_version` | string | No | Internal override for schema version. | | `tool_version` | string | No | Internal override for tool implementation version. | ## `notion_comments_fetch` [Section titled “notion\_comments\_fetch”](#notion_comments_fetch) Fetch comments for a given Notion block. Provide a `block_id` (the target page/block ID, hyphenated UUID). Supports pagination via `start_cursor` and `page_size` (1–100). LLM tip: extract `block_id` from a Notion URL’s trailing 32-char id, then insert hyphens (8-4-4-4-12). | Name | Type | Required | Description | | ---------------- | ------- | -------- | ------------------------------------------------------------------------------ | | `block_id` | string | Yes | Target Notion block (or page) ID to fetch comments for. Use a hyphenated UUID. | | `notion_version` | string | No | Optional Notion-Version header override (e.g., 2022-06-28). | | `page_size` | integer | No | Maximum number of comments to return (1–100). | | `schema_version` | string | No | Internal override for schema version. | | `start_cursor` | string | No | Cursor to fetch the next page of results. | | `tool_version` | string | No | Internal override for tool implementation version. | ## `notion_data_fetch` [Section titled “notion\_data\_fetch”](#notion_data_fetch) Fetch data from Notion using the workspace search API (/search). Supports pagination via start\_cursor. | Name | Type | Required | Description | | ---------------- | ------- | -------- | ---------------------------------------------------------------- | | `page_size` | integer | No | Max number of results to return (1–100) | | `query` | string | No | Text query used by /search | | `schema_version` | string | No | Optional schema version to use for tool execution | | `start_cursor` | string | No | Cursor for pagination; pass the previous response’s next\_cursor | | `tool_version` | string | No | Optional tool version to use for execution | ## `notion_data_source_fetch` [Section titled “notion\_data\_source\_fetch”](#notion_data_source_fetch) Retrieve a Notion database’s schema, title, and properties using the Notion 2025-09-03 API. Unlike notion\_database\_fetch, this returns a data\_sources array — each entry contains a data\_source\_id required by notion\_data\_source\_query and notion\_data\_source\_insert\_row. Use this as the first step when working with merged, synced, or multi-source databases. | Name | Type | Required | Description | | ------------- | ------ | -------- | -------------------------------------------------- | | `database_id` | string | Yes | The target database ID in UUID format with hyphens | ## `notion_data_source_insert_row` [Section titled “notion\_data\_source\_insert\_row”](#notion_data_source_insert_row) Create a new row (page) in a Notion data source using the 2025-09-03 API. Required for merged, synced, or multi-source databases — these require parent.data\_source\_id instead of parent.database\_id which the older notion\_database\_insert\_row uses. | Name | Type | Required | Description | | ---------------- | ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | | `data_source_id` | string | Yes | The ID of the data source to insert a row into. Retrieve from notion\_data\_source\_fetch response under data\_sources\[].id | | `properties` | `object` | Yes | Object mapping column names (or property ids) to property values | | `child_blocks` | `array` | No | Optional array of Notion blocks to append as page content | | `cover` | `object` | No | Optional page cover object (external/file) | | `icon` | `object` | No | Optional page icon object (emoji/external/file) | ## `notion_data_source_query` [Section titled “notion\_data\_source\_query”](#notion_data_source_query) Query rows (pages) from a Notion data source using the 2025-09-03 API. Required for merged, synced, or multi-source databases — these cannot be queried via notion\_database\_query as that tool uses the older `/databases/{id}/query` endpoint which does not support multiple data sources. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `data_source_id` | string | Yes | The ID of the data source to query. Retrieve from notion\_data\_source\_fetch response under data\_sources\[].id | | `filter` | `object` | No | Notion filter object to narrow results. Supports compound filters with ‘and’/‘or’ arrays | | `page_size` | integer | No | Maximum number of rows to return (1-100). Default: 10 | | `sorts` | `array` | No | Order the results. Each item must include either `property` or `timestamp`, plus `direction` | | `start_cursor` | string | No | Cursor to fetch the next page of results | ## `notion_database_create` [Section titled “notion\_database\_create”](#notion_database_create) Create a new database in Notion under a parent page. Provide a parent object with page\_id, a database title (rich\_text array), and a properties object that defines the database schema (columns). | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `notion_version` | string | No | Optional override for the Notion-Version header (e.g., 2022-06-28). | | `parent` | `object` | Yes | Parent object specifying the page under which the database is created. Example: `{"page_id": "2561ab6c-418b-8072-beec-c4779fa811cf"}` | | `properties` | `object` | Yes | Database schema object defining properties (columns). Example: `{"Name": {"title": {}}, "Status": {"select": {"options": [{"name": "Todo"}, {"name": "Doing"}, {"name": "Done"}]}}}` | | `schema_version` | string | No | Internal override for schema version. | | `title` | `array` | Yes | Database title as a Notion rich\_text array. | | `tool_version` | string | No | Internal override for tool implementation version. | ## `notion_database_fetch` [Section titled “notion\_database\_fetch”](#notion_database_fetch) Retrieve a Notion database’s full definition, including title, properties, and schema. Required: `database_id` (hyphenated UUID). LLM tip: Extract the last 32 characters from a Notion database URL, then insert hyphens (8-4-4-4-12). | Name | Type | Required | Description | | ---------------- | ------ | -------- | --------------------------------------------------- | | `database_id` | string | Yes | The target database ID in UUID format with hyphens. | | `notion_version` | string | No | Optional override for the Notion-Version header. | | `schema_version` | string | No | Optional schema version override. | | `tool_version` | string | No | Optional tool version override. | ## `notion_database_insert_row` [Section titled “notion\_database\_insert\_row”](#notion_database_insert_row) Insert a new row (page) into a Notion database. Required: `database_id` (hyphenated UUID) and `properties` (object mapping database column names to Notion \*\*property values). Optional: child\_blocks`(content blocks),`icon`(page icon object), and`cover\` (page cover object). LLM guidance: * `properties` must use **property values** (not schema). Example: ```json 1 { 2 "title": { "title": \[ { "text": { "content": "Task A" } } ] }, 3 "Status": { "select": { "name": "Todo" } }, 4 "Due": { "date": { "start": "2025-09-01" } } 5 } ``` * Use the **exact property key** as defined in the database (case‑sensitive), or the property id. * `icon` example (emoji): `{"type":"emoji","emoji":"📝"}` * `cover` example (external): `{"type":"external","external":{"url":"https://example.com/image.jpg"}}` * Runtime note: the executor/host should synthesize `parent = {"database_id": database_id}` before sending to Notion. | Name | Type | Required | Description | | ---------------- | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `_parent` | `object` | No | Computed by host: `{ "database_id": "" }`. Do not supply manually. | | `child_blocks` | `array` | No | Optional array of Notion blocks to append as page content (paragraph, heading, to\_do, etc.). | | `cover` | `object` | No | Optional page cover object. Example external: `{"type":"external","external":{"url":"https://example.com/cover.jpg"}}`. | | `database_id` | string | Yes | Target database ID (hyphenated UUID). | | `icon` | `object` | No | Optional page icon object. Examples: `{"type":"emoji","emoji":"📝"}` or `{"type":"external","external":{"url":"https://..."}}`. | | `notion_version` | string | No | Optional Notion-Version header override (e.g., 2022-06-28). | | `properties` | `object` | Yes | Object mapping **column names (or property ids)** to **property values**. ️ **CRITICAL: Property Identification Rules:** - For title fields: ALWAYS use ‘title’ as the property key (not ‘Name’ or display names) - For other properties: Use exact property names from database schema (case-sensitive) - DO NOT use URL-encoded property IDs with special characters **Recommended Workflow:** 1. Call fetch\_database first to see exact property names 2. Use ‘title’ for title-type properties 3. Match other property names exactly as shown in schema Example: `{ "title": { "title": [ { "text": { "content": "Task A" } } ] }, "Status": { "select": { "name": "Todo" } }, "Due": { "date": { "start": "2025-09-01" } } }` | | `schema_version` | string | No | Optional schema version override. | | `tool_version` | string | No | Optional tool version override. | ## `notion_database_property_retrieve` [Section titled “notion\_database\_property\_retrieve”](#notion_database_property_retrieve) Query a Notion database and return only specific properties by supplying one or more property IDs. Use when you need page rows but want to limit the returned properties to reduce payload. Provide the database\_id and an array of filter\_properties (each item is a property id like “title”) | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------------------------------------------------------------------------------------------- | | `database_id` | string | Yes | Target database ID (hyphenated UUID). | | `property_id` | string | No | property ID to filter results by a specific property. get the property id by querying database. | | `schema_version` | string | No | Optional schema version override. | | `tool_version` | string | No | Optional tool version override. | ## `notion_database_query` [Section titled “notion\_database\_query”](#notion_database_query) Query a Notion database for rows (pages). Provide database\_id (hyphenated UUID). Optional: page\_size, start\_cursor for pagination, and sorts (array of sort objects). LLM guidance: extract the last 32 characters from a Notion database URL and insert hyphens (8-4-4-4-12) to form database\_id. Sort rules: each sort item MUST include either property OR timestamp (last\_edited\_time/created\_time), not both. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | --------------------------------------------------------------------------------------- | | `database_id` | string | Yes | Target database ID (hyphenated UUID). | | `notion_version` | string | No | Optional Notion-Version header override. | | `page_size` | integer | No | Maximum number of rows to return (1–100). | | `schema_version` | string | No | Optional schema version override. | | `sorts` | `array` | No | Order the results. Each item must include either property or timestamp, plus direction. | | `start_cursor` | string | No | Cursor to fetch the next page of results. | | `tool_version` | string | No | Optional tool version override. | ## `notion_database_update` [Section titled “notion\_database\_update”](#notion_database_update) Update a Notion database’s title, description, or property schema. | Name | Type | Required | Description | | ------------- | -------- | -------- | ------------------------------------------------------------- | | `database_id` | string | Yes | The ID of the database to update | | `description` | string | No | New description for the database | | `properties` | `object` | No | Property schema updates (add, rename, or reconfigure columns) | | `title` | string | No | New title for the database | ## `notion_page_content_append` [Section titled “notion\_page\_content\_append”](#notion_page_content_append) Append blocks to a Notion page or block. Supports paragraph, heading\_1/2/3, bulleted\_list\_item, numbered\_list\_item, code, quote, callout, and divider block types. | Name | Type | Required | Description | | ---------- | --------------- | -------- | --------------------------------------------------------------------------------------------------- | | `block_id` | string | Yes | The ID of the page or block to append content to | | `blocks` | `array` | Yes | Array of blocks to append. Each block has a type and text (plus optional language for code blocks). | ## `notion_page_content_get` [Section titled “notion\_page\_content\_get”](#notion_page_content_get) Retrieve the content (blocks) of a Notion page or block. Returns all child blocks with their type and text content. | Name | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------------------------ | | `block_id` | string | Yes | The ID of the page or block whose children to retrieve | | `page_size` | number | No | Number of blocks to return (max 100) | | `start_cursor` | string | No | Cursor for pagination from a previous response | ## `notion_page_create` [Section titled “notion\_page\_create”](#notion_page_create) Create a page in Notion either inside a database (as a row) or as a child of a page. Use exactly one parent mode: provide database\_id to create a database row (page with properties) OR provide parent\_page\_id to create a child page. When creating in a database, properties must use Notion property value shapes and the title property key must be “title” (not the display name). Children (content blocks), icon, and cover are optional. The executor should synthesize the Notion parent object from the chosen parent input. Target rules: * Use database\_id OR parent\_page\_id (not both) * If database\_id is provided → properties are required * If parent\_page\_id is provided → properties are optional | Name | Type | Required | Description | | ---------------- | ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `_parent` | `object` | No | Computed by the executor: `{"database_id": "..."}` OR `{"page_id": "..."}` derived from database\_id/parent\_page\_id. | | `child_blocks` | `array` | No | Optional blocks to add as page content (children). | | `cover` | `object` | No | Optional page cover object. | | `database_id` | string | No | Create a page as a new row in this database (hyphenated UUID). Extract from the database URL (last 32 chars → hyphenate 8-4-4-4-12). | | `icon` | `object` | No | Optional page icon object. | | `notion_version` | string | No | Optional Notion-Version header override. | | `parent_page_id` | string | No | Create a child page under this page (hyphenated UUID). Extract from the parent page URL. | | `properties` | `object` | No | For database rows, supply property values keyed by property name (or id). For title properties, the key must be “title”. Example (database row): `{ "title": { "title": [ { "text": { "content": "Task A" } } ] }, "Status": { "select": { "name": "Todo" } }, "Due": { "date": { "start": "2025-09-01" } } }` | | `schema_version` | string | No | Optional schema version override. | | `tool_version` | string | No | Optional tool version override. | ## `notion_page_get` [Section titled “notion\_page\_get”](#notion_page_get) Retrieve a Notion page by its ID. Returns the page properties, metadata, and parent information. | Name | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------- | | `page_id` | string | Yes | The ID of the Notion page to retrieve | ## `notion_page_search` [Section titled “notion\_page\_search”](#notion_page_search) Search Notion pages by text query. Returns matching pages with their titles, IDs, and metadata. Optionally sort by last\_edited\_time or created\_time, and paginate with start\_cursor. | Name | Type | Required | Description | | ---------------- | ------- | -------- | -------------------------------------------------------------------------------------------- | | `query` | string | No | Text to search for across Notion pages | | `page_size` | integer | No | Maximum number of pages to return (1-100). Default: 10 | | `sort_direction` | string | No | Direction to sort results: ascending or descending. Default: descending | | `sort_timestamp` | string | No | Timestamp field to sort by: last\_edited\_time or created\_time. Default: last\_edited\_time | | `start_cursor` | string | No | Cursor to fetch the next page of results | ## `notion_page_update` [Section titled “notion\_page\_update”](#notion_page_update) Update a Notion page’s properties, archive/unarchive it, or change its icon and cover. | Name | Type | Required | Description | | ------------ | -------- | -------- | --------------------------------------------------------------- | | `archived` | boolean | No | Set to true to archive (delete) the page, false to unarchive it | | `cover` | `object` | No | Page cover image to set | | `icon` | `object` | No | Page icon to set | | `page_id` | string | Yes | The ID of the Notion page to update | | `properties` | `object` | No | Page properties to update using Notion property value shapes | ## `notion_user_list` [Section titled “notion\_user\_list”](#notion_user_list) List all users in the Notion workspace including people and bots. | Name | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------------------------- | | `page_size` | number | No | Number of users to return (max 100) | | `start_cursor` | string | No | Cursor for pagination from a previous response | --- # DOCUMENT BOUNDARY --- # OneDrive > Connect to OneDrive. Manage files, folders, and cloud storage with Microsoft OneDrive Connect to OneDrive. Manage files, folders, and cloud storage with Microsoft OneDrive ![OneDrive logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/onedrive.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the OneDrive connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **OneDrive** and click **Create**. Copy the redirect URI. It will look like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.DKbh4KLS.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Sign into [portal.azure.com](https://portal.azure.com) and go to **Azure Active Directory** → **App registrations** → **New registration**. * Enter a name for your app. * Under **Supported account types**, select **Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts**. * Under **Redirect URI**, select **Web** and paste the redirect URI from step 1. Click **Register**. ![Register an application in Azure portal](/.netlify/images?url=_astro%2Fadd-redirect-uri.DJAUScZr.png\&w=1440\&h=1200\&dpl=69cce21a4f77360008b1503a) 2. ### Get your client credentials * Go to **Certificates & secrets** → **New client secret**, set an expiry, and click **Add**. Copy the **Value** immediately. * From the **Overview** page, copy the **Application (client) ID**. 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (Application (client) ID from Azure) * Client Secret (from Certificates & secrets) * Permissions (scopes — see [Microsoft Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-reference)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.HJl-c2GR.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s OneDrive account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'onedrive'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize OneDrive:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1.0/me/drive', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "onedrive" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize OneDrive:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1.0/me/drive", 30 method="GET" 31 ) 32 print(result) ``` --- # DOCUMENT BOUNDARY --- # OneNote > Connect to Microsoft OneNote. Access, create, and manage notebooks, sections, and pages stored in OneDrive or SharePoint through Microsoft Graph API. Connect to Microsoft OneNote. Access, create, and manage notebooks, sections, and pages stored in OneDrive or SharePoint through Microsoft Graph API. ![OneNote logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/onenote.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Microsoft OneNote connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Create the OneNote connection in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Actions** → **Connections** and click **+ Create Connection**. Search for **OneNote** and click **Create**. ![Search for OneNote and create a new connection](/.netlify/images?url=_astro%2Fcreate-onenote-connection.B-sF1uoI.png\&w=3024\&h=1628\&dpl=69cce21a4f77360008b1503a) * In the **Configure OneNote Connection** dialog, copy the **Redirect URI**. You will need this when registering your app in Azure. ![Copy the redirect URI from the Configure OneNote Connection dialog](/.netlify/images?url=_astro%2Fconfigure-onenote-connection.B802AYbQ.png\&w=1536\&h=1620\&dpl=69cce21a4f77360008b1503a) 2. ### Register an application in Azure * Sign into [portal.azure.com](https://portal.azure.com) and go to **Microsoft Entra ID** → **App registrations**. ![App registrations page in Azure portal](/.netlify/images?url=_astro%2Fazure-app-registrations.Z9372CaQ.png\&w=3024\&h=1552\&dpl=69cce21a4f77360008b1503a) * Click **New registration**. Enter a name for your app (for example, “Scalekit\_Agent\_Actions”). * Under **Supported account types**, select **Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts**. * Under **Redirect URI**, select **Web** and paste the redirect URI you copied from the Scalekit dashboard. Click **Register**. ![Register an application with the Scalekit redirect URI in Azure](/.netlify/images?url=_astro%2Fazure-register-app-filled.Do6V-ixU.png\&w=3024\&h=1550\&dpl=69cce21a4f77360008b1503a) 3. ### Get your client credentials * From the app’s **Overview** page, copy the **Application (client) ID**. ![Copy the Application (client) ID from the Azure app overview](/.netlify/images?url=_astro%2Fazure-app-overview.Bk0hSWKg.png\&w=3024\&h=1560\&dpl=69cce21a4f77360008b1503a) * Go to **Certificates & secrets** in the left sidebar, then click **+ New client secret**. ![Certificates and secrets page in Azure portal](/.netlify/images?url=_astro%2Fazure-certificates-secrets.C0P6ZXjY.png\&w=3024\&h=1478\&dpl=69cce21a4f77360008b1503a) * Enter a description, set an expiry period, and click **Add**. Copy the secret **Value** immediately — it is only shown once. ![Add a client secret in Azure portal](/.netlify/images?url=_astro%2Fazure-add-client-secret.Dp3owI2F.png\&w=1172\&h=1476\&dpl=69cce21a4f77360008b1503a) 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Actions** → **Connections** and open the OneNote connection you created. * Enter your credentials: * **Client ID** — the Application (client) ID from the Azure app overview * **Client Secret** — the secret value from Certificates & secrets * **Scopes** — select the permissions your app needs (for example, `Notes.ReadWrite`, `User.Read`, `email`, `openid`, `profile`, `offline_access`). See [Microsoft Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-reference) for the full list. * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s OneNote account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'onenote'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize OneNote:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1.0/me/onenote/notebooks', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "onenote" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize OneNote:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1.0/me/onenote/notebooks", 30 method="GET" 31 ) 32 print(result) ``` --- # DOCUMENT BOUNDARY --- # Outlook > Connect to Microsoft Outlook. Manage emails, calendar events, contacts, and tasks Connect to Microsoft Outlook. Manage emails, calendar events, contacts, and tasks ![Outlook logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/outlook.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Outlook connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Create the Outlook connection in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Actions** → **Connections** and click **+ Create Connection**. Search for **Outlook** and click **Create**. ![Search for Outlook and create a new connection](/.netlify/images?url=_astro%2Fcreate-outlook-connection.2Fttb9Y6.png\&w=3024\&h=1622\&dpl=69cce21a4f77360008b1503a) * In the **Configure Outlook Connection** dialog, copy the **Redirect URI**. You will need this when registering your app in Azure. ![Copy the redirect URI from the Configure Outlook Connection dialog](/.netlify/images?url=_astro%2Fconfigure-outlook-connection.C0ZwF_P1.png\&w=1530\&h=1614\&dpl=69cce21a4f77360008b1503a) 2. ### Register an application in Azure * Sign into [portal.azure.com](https://portal.azure.com) and go to **Microsoft Entra ID** → **App registrations**. ![App registrations page in Azure portal](/.netlify/images?url=_astro%2Fazure-app-registrations.BqJzS2Xb.png\&w=3024\&h=1964\&dpl=69cce21a4f77360008b1503a) * Click **New registration**. Enter a name for your app (for example, “Scalekit Outlook Connector”). * Under **Supported account types**, select **Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts**. * Under **Redirect URI**, select **Web** and paste the redirect URI you copied from the Scalekit dashboard. Click **Register**. ![Paste the Scalekit redirect URI in Azure](/.netlify/images?url=_astro%2Fazure-add-redirect-uri.DmNcFjki.png\&w=1908\&h=400\&dpl=69cce21a4f77360008b1503a) 3. ### Get your client credentials * From the app’s **Overview** page, copy the **Application (client) ID**. ![Copy the Application (client) ID from the Azure app overview](/.netlify/images?url=_astro%2Fazure-app-overview.DHFrFlF5.png\&w=3024\&h=1546\&dpl=69cce21a4f77360008b1503a) * Go to **Certificates & secrets** in the left sidebar, then click **+ New client secret**. ![Certificates and secrets page in Azure portal](/.netlify/images?url=_astro%2Fazure-certificates-secrets.B1uv25n_.png\&w=3024\&h=1554\&dpl=69cce21a4f77360008b1503a) * Enter a description (for example, “Secret for Scalekit Agent Actions”), set an expiry period, and click **Add**. Copy the secret **Value** immediately — it is only shown once. ![Add a client secret in Azure portal](/.netlify/images?url=_astro%2Fazure-add-client-secret.2jYPaBFO.png\&w=1168\&h=1474\&dpl=69cce21a4f77360008b1503a) 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Actions** → **Connections** and open the Outlook connection you created. * Enter your credentials: * **Client ID** — the Application (client) ID from the Azure app overview * **Client Secret** — the secret value from Certificates & secrets * **Scopes** — select the permissions your app needs (for example, `Calendars.Read`, `Calendars.ReadWrite`, `Mail.Read`, `Mail.ReadWrite`, `Mail.Send`, `Contacts.Read`, `Contacts.ReadWrite`, `User.Read`, `offline_access`). See [Microsoft Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-reference) for the full list. * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Outlook account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Outlook in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'outlook'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Outlook:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1.0/me/messages', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "outlook" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Outlook:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1.0/me/messages", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `outlook_create_calendar_event` [Section titled “outlook\_create\_calendar\_event”](#outlook_create_calendar_event) Create a new calendar event in the user’s Outlook calendar. Supports attendees, recurrence, reminders, online meetings, multiple locations, and event properties. | Name | Type | Required | Description | | ---------------------------- | ------- | -------- | ------------------------------------------------------------------------ | | `attendees_optional` | string | No | Array of email addresses for optional attendees | | `attendees_required` | string | No | Array of email addresses for required attendees | | `attendees_resource` | string | No | Array of email addresses for resources (meeting rooms, equipment) | | `body_content` | string | No | No description | | `body_contentType` | string | No | No description | | `end_datetime` | string | Yes | No description | | `end_timezone` | string | Yes | No description | | `hideAttendees` | boolean | No | When true, each attendee only sees themselves | | `importance` | string | No | Event importance level | | `isAllDay` | boolean | No | Mark as all-day event | | `isOnlineMeeting` | boolean | No | Create an online meeting (Teams/Skype) | | `isReminderOn` | boolean | No | Enable or disable reminder | | `location` | string | No | No description | | `locations` | string | No | JSON array of location objects with displayName, address, coordinates | | `onlineMeetingProvider` | string | No | Online meeting provider | | `recurrence_days_of_week` | string | No | Days of week for weekly recurrence (comma-separated) | | `recurrence_end_date` | string | No | End date for recurrence (YYYY-MM-DD), required if range\_type is endDate | | `recurrence_interval` | integer | No | How often the event recurs (e.g., every 2 weeks = 2) | | `recurrence_occurrences` | integer | No | Number of occurrences, required if range\_type is numbered | | `recurrence_range_type` | string | No | How the recurrence ends | | `recurrence_start_date` | string | No | Start date for recurrence (YYYY-MM-DD) | | `recurrence_type` | string | No | Recurrence pattern type | | `reminderMinutesBeforeStart` | integer | No | Minutes before event start to show reminder | | `sensitivity` | string | No | Event sensitivity/privacy level | | `showAs` | string | No | Free/busy status | | `start_datetime` | string | Yes | No description | | `start_timezone` | string | Yes | No description | | `subject` | string | Yes | No description | ## `outlook_create_contact` [Section titled “outlook\_create\_contact”](#outlook_create_contact) Create a new contact in the user’s mailbox with name, email addresses, and phone numbers. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ------------------------------------------------------------------------ | | `givenName` | string | Yes | First name of the contact | | `surname` | string | Yes | Last name of the contact | | `emailAddresses` | array | No | Array of email address objects with ‘address’ and optional ‘name’ fields | | `businessPhones` | array | No | Array of business phone numbers | | `mobilePhone` | string | No | Mobile phone number | | `jobTitle` | string | No | Job title | | `companyName` | string | No | Company name | ## `outlook_delete_calendar_event` [Section titled “outlook\_delete\_calendar\_event”](#outlook_delete_calendar_event) Delete a calendar event by ID. | Name | Type | Required | Description | | ---------- | ------ | -------- | -------------- | | `event_id` | string | Yes | No description | ## `outlook_get_attachment` [Section titled “outlook\_get\_attachment”](#outlook_get_attachment) Download a specific attachment from an Outlook email message by attachment ID. Returns the full attachment including base64-encoded file content in the contentBytes field. | Name | Type | Required | Description | | --------------- | ------ | -------- | ----------------------------------------------- | | `message_id` | string | Yes | The ID of the message containing the attachment | | `attachment_id` | string | Yes | The ID of the attachment to download | ## `outlook_get_calendar_event` [Section titled “outlook\_get\_calendar\_event”](#outlook_get_calendar_event) Retrieve an existing calendar event by ID from the user’s Outlook calendar. | Name | Type | Required | Description | | ---------- | ------ | -------- | -------------- | | `event_id` | string | Yes | No description | ## `outlook_get_message` [Section titled “outlook\_get\_message”](#outlook_get_message) Retrieve a specific email message by ID from the user’s Outlook mailbox, including full body content, sender, recipients, attachments info, and metadata. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------- | | `message_id` | string | Yes | The ID of the message to retrieve | ## `outlook_list_attachments` [Section titled “outlook\_list\_attachments”](#outlook_list_attachments) List all attachments on a specific Outlook email message. Returns attachment metadata including ID, name, size, and content type. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------- | | `message_id` | string | Yes | The ID of the message to list attachments for | ## `outlook_list_calendar_events` [Section titled “outlook\_list\_calendar\_events”](#outlook_list_calendar_events) List calendar events from the user’s Outlook calendar with filtering, sorting, pagination, and field selection. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------------------------------------- | | `filter` | string | No | OData filter expression to filter events (e.g., startsWith(subject,‘All’)) | | `orderby` | string | No | OData orderby expression to sort events (e.g., start/dateTime desc) | | `select` | string | No | Comma-separated list of properties to include in the response | | `skip` | number | No | Number of events to skip for pagination | | `top` | number | No | Maximum number of events to return | ## `outlook_update_calendar_event` [Section titled “outlook\_update\_calendar\_event”](#outlook_update_calendar_event) Update an existing Outlook calendar event. Only provided fields will be updated. Supports time, attendees, location, reminders, online meetings, recurrence, and event properties. | Name | Type | Required | Description | | ---------------------------- | ------- | -------- | --------------------------------------------------------------------- | | `attendees_optional` | string | No | Comma-separated optional attendee emails | | `attendees_required` | string | No | Comma-separated required attendee emails | | `attendees_resource` | string | No | Comma-separated resource emails (meeting rooms, equipment) | | `body_content` | string | No | Event description/body | | `body_contentType` | string | No | Content type of body | | `categories` | string | No | Comma-separated categories | | `end_datetime` | string | No | Event end time in RFC3339 format | | `end_timezone` | string | No | Timezone for end time | | `event_id` | string | Yes | The ID of the calendar event to update | | `hideAttendees` | boolean | No | When true, each attendee only sees themselves | | `importance` | string | No | Event importance level | | `isAllDay` | boolean | No | Mark as all-day event | | `isOnlineMeeting` | boolean | No | Create an online meeting (Teams/Skype) | | `isReminderOn` | boolean | No | Enable or disable reminder | | `location` | string | No | Physical or virtual location | | `locations` | string | No | JSON array of location objects with displayName, address, coordinates | | `onlineMeetingProvider` | string | No | Online meeting provider | | `recurrence_days_of_week` | string | No | Days of week for weekly recurrence (comma-separated) | | `recurrence_end_date` | string | No | End date for recurrence (YYYY-MM-DD) | | `recurrence_interval` | integer | No | How often the event recurs (e.g., every 2 weeks = 2) | | `recurrence_occurrences` | integer | No | Number of occurrences | | `recurrence_range_type` | string | No | How the recurrence ends | | `recurrence_start_date` | string | No | Start date for recurrence (YYYY-MM-DD) | | `recurrence_type` | string | No | Recurrence pattern type | | `reminderMinutesBeforeStart` | integer | No | Minutes before event start to show reminder | | `sensitivity` | string | No | Event sensitivity/privacy level | | `showAs` | string | No | Free/busy status | | `start_datetime` | string | No | Event start time in RFC3339 format | | `start_timezone` | string | No | Timezone for start time | | `subject` | string | No | Event title/summary | ## `outlook_list_contacts` [Section titled “outlook\_list\_contacts”](#outlook_list_contacts) List all contacts in the user’s mailbox with support for filtering, pagination, and field selection. | Name | Type | Required | Description | | ---------- | ------- | -------- | -------------------------------------------- | | `$filter` | string | No | Filter expression to narrow results (OData) | | `$orderby` | string | No | Property to sort by (e.g., “displayName”) | | `$select` | string | No | Comma-separated list of properties to return | | `$skip` | integer | No | Number of contacts to skip for pagination | | `$top` | integer | No | Number of contacts to return (default: 10) | ## `outlook_list_messages` [Section titled “outlook\_list\_messages”](#outlook_list_messages) List all messages in the user’s mailbox with support for filtering, pagination, and field selection. Returns 10 messages by default. | Name | Type | Required | Description | | ---------- | ------- | -------- | --------------------------------------------------- | | `$filter` | string | No | Filter expression to narrow results (OData) | | `$orderby` | string | No | Property to sort by (e.g., “receivedDateTime desc”) | | `$select` | string | No | Comma-separated list of properties to return | | `$skip` | integer | No | Number of messages to skip for pagination | | `$top` | integer | No | Number of messages to return (1-1000, default: 10) | ## `outlook_mailbox_settings_get` [Section titled “outlook\_mailbox\_settings\_get”](#outlook_mailbox_settings_get) Retrieve the mailbox settings for the signed-in user. Returns automatic replies (out-of-office) configuration, language, timezone, working hours, date/time format, and delegate meeting message delivery preferences. ## `outlook_mailbox_settings_update` [Section titled “outlook\_mailbox\_settings\_update”](#outlook_mailbox_settings_update) Update mailbox settings for the signed-in user. Supports configuring automatic replies (out-of-office), language, timezone, working hours, date/time format, and delegate meeting message delivery preferences. Only fields provided will be updated. | Name | Type | Required | Description | | --------------------------------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `automaticRepliesSetting` | `object` | No | Configuration for automatic replies (out-of-office). Includes status, internalReplyMessage, externalReplyMessage, externalAudience, scheduledStartDateTime, scheduledEndDateTime | | `dateFormat` | string | No | Preferred date format string (e.g., ‘MM/dd/yyyy’) | | `delegateMeetingMessageDeliveryOptions` | string | No | Controls how meeting messages are delivered when a delegate is configured | | `language` | `object` | No | Language and locale. Properties: locale (string), displayName (string) | | `timeFormat` | string | No | Preferred time format string (e.g., ‘hh:mm tt’ or ‘HH:mm’) | | `timeZone` | string | No | Preferred time zone (e.g., ‘UTC’, ‘Pacific Standard Time’) | | `workingHours` | `object` | No | Working hours config. Properties: daysOfWeek (array), startTime, endTime, timeZone | ## `outlook_reply_to_message` [Section titled “outlook\_reply\_to\_message”](#outlook_reply_to_message) Reply to an existing email message. The reply is automatically sent to the original sender and saved in the Sent Items folder. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------ | | `messageId` | string | Yes | The unique identifier of the message to reply to | | `comment` | string | Yes | Reply message content | ## `outlook_search_messages` [Section titled “outlook\_search\_messages”](#outlook_search_messages) Search messages by keywords across subject, body, sender, and other fields. Returns matching messages with support for pagination. | Name | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------------------- | | `query` | string | Yes | Search query string (searches across subject, body, from, to) | | `$select` | string | No | Comma-separated list of properties to return | | `$skip` | integer | No | Number of messages to skip for pagination | | `$top` | integer | No | Number of messages to return (1-1000, default: 10) | ## `outlook_send_message` [Section titled “outlook\_send\_message”](#outlook_send_message) Send an email message using Microsoft Graph API. The message is saved in the Sent Items folder by default. | Name | Type | Required | Description | | ----------------- | --------------- | -------- | ----------------------------------------------------- | | `subject` | string | Yes | Subject line of the email | | `body` | string | Yes | Body content of the email | | `toRecipients` | `array` | Yes | Array of email addresses to send to | | `bodyType` | string | No | Content type of the body (Text or HTML) | | `ccRecipients` | array | No | Array of email addresses to CC | | `bccRecipients` | array | No | Array of email addresses to BCC | | `saveToSentItems` | boolean | No | Save the message in Sent Items folder (default: true) | ## `outlook_todo_lists_create` [Section titled “outlook\_todo\_lists\_create”](#outlook_todo_lists_create) Create a new Microsoft To Do task list. | Name | Type | Required | Description | | -------------- | ------ | -------- | ------------------------- | | `display_name` | string | Yes | The name of the task list | ## `outlook_todo_lists_delete` [Section titled “outlook\_todo\_lists\_delete”](#outlook_todo_lists_delete) Permanently delete a Microsoft To Do task list and all its tasks. | Name | Type | Required | Description | | --------- | ------ | -------- | --------------------------------- | | `list_id` | string | Yes | The ID of the task list to delete | ## `outlook_todo_lists_get` [Section titled “outlook\_todo\_lists\_get”](#outlook_todo_lists_get) Get a specific Microsoft To Do task list by ID. | Name | Type | Required | Description | | --------- | ------ | -------- | ----------------------- | | `list_id` | string | Yes | The ID of the task list | ## `outlook_todo_lists_list` [Section titled “outlook\_todo\_lists\_list”](#outlook_todo_lists_list) List all Microsoft To Do task lists for the current user. ## `outlook_todo_lists_update` [Section titled “outlook\_todo\_lists\_update”](#outlook_todo_lists_update) Rename a Microsoft To Do task list. | Name | Type | Required | Description | | -------------- | ------ | -------- | --------------------------------- | | `list_id` | string | Yes | The ID of the task list to update | | `display_name` | string | Yes | The new name for the task list | ## `outlook_todo_tasks_create` [Section titled “outlook\_todo\_tasks\_create”](#outlook_todo_tasks_create) Create a new task in a Microsoft To Do task list with optional body, due date, importance, and reminder. | Name | Type | Required | Description | | -------------------- | ------ | -------- | --------------------------------------------------------------------------- | | `list_id` | string | Yes | The ID of the task list to add the task to | | `title` | string | Yes | The title of the task | | `body` | string | No | The body/notes of the task (plain text) | | `importance` | string | No | The importance of the task: low, normal, or high | | `status` | string | No | The status: notStarted, inProgress, completed, waitingOnOthers, or deferred | | `due_date` | string | No | Due date in YYYY-MM-DD format | | `due_time_zone` | string | No | Time zone for the due date (defaults to UTC) | | `reminder_date_time` | string | No | Reminder date and time in ISO 8601 format | | `reminder_time_zone` | string | No | Time zone for the reminder (defaults to UTC) | | `categories` | array | No | Array of category names to assign to the task | ## `outlook_todo_tasks_delete` [Section titled “outlook\_todo\_tasks\_delete”](#outlook_todo_tasks_delete) Permanently delete a task from a Microsoft To Do task list. | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------- | | `list_id` | string | Yes | The ID of the task list | | `task_id` | string | Yes | The ID of the task to delete | ## `outlook_todo_tasks_get` [Section titled “outlook\_todo\_tasks\_get”](#outlook_todo_tasks_get) Get a specific task from a Microsoft To Do task list. | Name | Type | Required | Description | | --------- | ------ | -------- | ----------------------- | | `list_id` | string | Yes | The ID of the task list | | `task_id` | string | Yes | The ID of the task | ## `outlook_todo_tasks_list` [Section titled “outlook\_todo\_tasks\_list”](#outlook_todo_tasks_list) List all tasks in a Microsoft To Do task list with optional filtering and pagination. | Name | Type | Required | Description | | ---------- | ------- | -------- | -------------------------------------------------------- | | `list_id` | string | Yes | The ID of the task list | | `$top` | integer | No | Number of tasks to return (default: 10) | | `$skip` | integer | No | Number of tasks to skip for pagination | | `$filter` | string | No | OData filter expression (e.g., “status eq ‘notStarted’“) | | `$orderby` | string | No | Property to sort by (e.g., “createdDateTime desc”) | ## `outlook_todo_tasks_update` [Section titled “outlook\_todo\_tasks\_update”](#outlook_todo_tasks_update) Update a task in a Microsoft To Do task list. Only provided fields are changed. | Name | Type | Required | Description | | --------------- | ------ | -------- | --------------------------------------------------------------------------- | | `list_id` | string | Yes | The ID of the task list | | `task_id` | string | Yes | The ID of the task to update | | `title` | string | No | New title for the task | | `body` | string | No | New body/notes for the task (plain text) | | `importance` | string | No | The importance: low, normal, or high | | `status` | string | No | The status: notStarted, inProgress, completed, waitingOnOthers, or deferred | | `due_date` | string | No | Due date in YYYY-MM-DD format | | `due_time_zone` | string | No | Time zone for the due date (defaults to UTC) | | `categories` | array | No | Array of category names to assign | --- # DOCUMENT BOUNDARY --- # PhantomBuster > Connect to PhantomBuster. Launch and manage automation agents, stream container output, manage leads and lead lists, run AI completions, export usage reports, and control your entire organization programmatically. Connect to PhantomBuster to launch and monitor automation agents, stream live container output, manage leads and lead lists, run AI completions grounded in real data, export usage reports, and control every aspect of your PhantomBuster organization programmatically. ![PhantomBuster logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/phantombuster.svg) Supports authentication: API Key Plan requirements PhantomBuster tools require different subscription tiers. AI completions require **AI credits** (Pro+). CRM contact saving requires a configured **HubSpot integration** (Pro+). Branch management requires **Team+**. See the setup guide below for the full plan breakdown. What you can build with this connector PhantomBuster automates web actions — primarily LinkedIn scraping, lead generation, and outreach at scale. With this connector your AI agent can: | Use case | Tools involved | | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | **LinkedIn outreach pipeline** | `phantombuster_agent_launch` → `phantombuster_container_fetch_result` → `phantombuster_leads_save_many` | | **Scheduled scraping with monitoring** | `phantombuster_agent_save` (set schedule) → `phantombuster_org_fetch_running_containers` → `phantombuster_container_fetch_output` | | **Lead enrichment with AI** | `phantombuster_leads_fetch_by_list` → `phantombuster_ai_completions` (parse titles/skills) → `phantombuster_leads_save_many` | | **Quota-aware automation** | `phantombuster_org_fetch_resources` (check limits) → `phantombuster_agent_launch` → `phantombuster_org_export_container_usage` | | **Multi-org reporting** | `phantombuster_org_fetch` → `phantombuster_org_export_agent_usage` → `phantombuster_org_export_container_usage` | **Key concepts:** * **Agent** — a saved automation configuration (script + schedule + arguments). Launch an agent to create a container. * **Container** — a single execution run of an agent. Containers hold logs, status, and result data. * **Lead list** — a named collection of leads stored in your PhantomBuster organization. ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your PhantomBuster API key with Scalekit so it can authenticate and proxy automation requests on behalf of your users. PhantomBuster uses API key authentication — there is no redirect URI or OAuth flow. 1. ### Get your PhantomBuster API key * Sign in to [phantombuster.com](https://phantombuster.com) and go to **Settings** → **API** in the left sidebar. * Your API key is displayed on this page. Click the copy icon to copy it. If you need a new key, click **Regenerate**. ![PhantomBuster Settings API page showing the API key field with copy button](/.netlify/images?url=_astro%2Fcreate-api-key.DfpreEZ8.png\&w=1100\&h=520\&dpl=69cce21a4f77360008b1503a) Keep your API key secret Your PhantomBuster API key grants full access to your organization — including launching agents, reading lead data, and managing billing. Never expose it in client-side code or commit it to source control. Plan requirements PhantomBuster tools require different subscription tiers. Review the table below before choosing a plan at [phantombuster.com/pricing](https://phantombuster.com/pricing): | Plan | Included resources | Notes | | ------------ | ----------------------------------- | ----------------------------------------- | | **Trial** | 2 hrs execution time/month, 1 agent | Core agent and container tools only | | **Starter** | 20 hrs/month, up to 5 agents | Leads, lists, basic org tools | | **Pro** | 80 hrs/month, up to 15 agents | CRM integration, AI credits, SERP credits | | **Team** | 300 hrs/month, unlimited agents | Custom scripts, branches, full org export | | **Business** | Custom | SSO, dedicated support, custom limits | Specific tool requirements: * `phantombuster_ai_completions` — requires **AI credits** (included in Pro+, or purchasable add-on) * `phantombuster_org_save_crm_contact` — requires a **HubSpot CRM integration** configured in org settings (Pro+) * `phantombuster_branch_*` — requires **custom script access** (Team+) * `phantombuster_org_export_*` — requires **Pro+** for full date ranges (up to 6 months) 2. ### Create a connection in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **PhantomBuster** and click **Create**. * Note the **Connection name** — you will use this as `connection_name` in your code (e.g., `phantombuster`). ![Scalekit connection configuration page for PhantomBuster showing the connection name and API Key authentication type](/.netlify/images?url=_astro%2Fadd-credentials.B7KwtyQS.png\&w=1000\&h=360\&dpl=69cce21a4f77360008b1503a) 3. ### Add a connected account Connected accounts link a specific user identifier in your system to a PhantomBuster API key. Add accounts via the dashboard for testing, or via the Scalekit API in production. **Via dashboard (for testing)** * Open the connection you created and click the **Connected Accounts** tab → **Add account**. * Fill in: * **Your User’s ID** — a unique identifier for this user in your system (e.g., `user_123`) * **API Key** — the PhantomBuster API key you copied in step 1 * Click **Save**. ![Add connected account form for PhantomBuster in Scalekit dashboard showing User ID and API Key fields](/.netlify/images?url=_astro%2Fadd-connected-account.BB3b_ez6.png\&w=1000\&h=440\&dpl=69cce21a4f77360008b1503a) **Via API (for production)** * Node.js ```typescript 1 await scalekit.actions.upsertConnectedAccount({ 2 connectionName: 'phantombuster', 3 identifier: 'user_123', 4 credentials: { api_key: 'your-phantombuster-api-key' }, 5 }); ``` * Python ```python 1 scalekit_client.actions.upsert_connected_account( 2 connection_name="phantombuster", 3 identifier="user_123", 4 credentials={"api_key": "your-phantombuster-api-key"} 5 ) ``` Production usage tip In production, call `upsertConnectedAccount` when a user connects their PhantomBuster account — for example, after they paste their API key into a settings page in your app. Rate limits PhantomBuster enforces per-plan API rate limits. Exceeding them returns `429 Too Many Requests`. Monitor your execution time and resource usage at [phantombuster.com](https://phantombuster.com) → **Dashboard** → **Usage**. ## Usage [Section titled “Usage”](#usage) Once a connected account is set up, call the PhantomBuster API through the Scalekit proxy. Scalekit injects the API key as the `X-Phantombuster-Key` header automatically — you never handle credentials in your application code. You can interact with PhantomBuster in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'phantombuster'; // connection name from your Scalekit dashboard 5 const identifier = 'user_123'; // your user's unique identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // List all agents via Scalekit proxy — no API key needed here 16 const result = await actions.request({ 17 connectionName, 18 identifier, 19 path: '/api/v2/agents', 20 method: 'GET', 21 }); 22 console.log(result.data); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "phantombuster" # connection name from your Scalekit dashboard 6 identifier = "user_123" # your user's unique identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # List all agents via Scalekit proxy — no API key needed here 17 result = actions.request( 18 connection_name=connection_name, 19 identifier=identifier, 20 path="/api/v2/agents", 21 method="GET" 22 ) 23 print(result) ``` No OAuth flow needed PhantomBuster uses API key auth — unlike OAuth connectors, there is no authorization link or redirect flow. Once you call `upsertConnectedAccount` (or add an account via the dashboard), your users can make requests immediately. ## Scalekit tools Use `execute_tool` to call PhantomBuster tools directly from your code. Scalekit resolves the connected account, injects the API key, and returns a structured response — no raw HTTP or credential management needed. ### Launch an agent and retrieve results The most common PhantomBuster workflow: launch an automation agent, stream its console output while it runs, then read the final result. examples/phantombuster\_launch.py ```python 1 import scalekit.client, os, time 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 scalekit_client = scalekit.client.ScalekitClient( 6 client_id=os.getenv("SCALEKIT_CLIENT_ID"), 7 client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), 8 env_url=os.getenv("SCALEKIT_ENV_URL"), 9 ) 10 actions = scalekit_client.actions 11 12 connection_name = "phantombuster" 13 identifier = "user_123" 14 15 # Step 1: Resolve the connected account for this user 16 response = actions.get_or_create_connected_account( 17 connection_name=connection_name, 18 identifier=identifier 19 ) 20 connected_account = response.connected_account 21 22 # Step 2: Find the agent you want to run 23 agents = actions.execute_tool( 24 tool_name="phantombuster_agents_fetch_all", 25 connected_account_id=connected_account.id, 26 tool_input={} 27 ) 28 agent_id = agents.result[0]["id"] # pick the first agent, or filter by name 29 30 # Step 3: Launch the agent 31 launch_result = actions.execute_tool( 32 tool_name="phantombuster_agent_launch", 33 connected_account_id=connected_account.id, 34 tool_input={ 35 "id": agent_id, 36 "output": "result-object", 37 } 38 ) 39 container_id = launch_result.result["containerId"] 40 print(f"Agent launched. Container ID: {container_id}") 41 42 # Step 4: Poll for output until the agent finishes 43 output_pos = 0 44 while True: 45 output = actions.execute_tool( 46 tool_name="phantombuster_container_fetch_output", 47 connected_account_id=connected_account.id, 48 tool_input={"id": container_id, "fromOutputPos": output_pos} 49 ) 50 print(output.result.get("output", ""), end="", flush=True) 51 output_pos = output.result.get("nextOutputPos", output_pos) 52 if output.result.get("status") in ("finished", "error"): 53 break 54 time.sleep(3) # poll every 3 seconds 55 56 # Step 5: Fetch the structured result 57 final_result = actions.execute_tool( 58 tool_name="phantombuster_container_fetch_result", 59 connected_account_id=connected_account.id, 60 tool_input={"id": container_id} 61 ) 62 print("Scraped profiles:", final_result.result) ``` ### Save scraped profiles as leads After a scraping run, bulk-save extracted profiles to a PhantomBuster lead list for downstream CRM enrichment or outreach. examples/phantombuster\_save\_leads.py ```python 1 # First: fetch available lead lists (or create one in the PhantomBuster dashboard) 2 lists = actions.execute_tool( 3 tool_name="phantombuster_lists_fetch_all", 4 connected_account_id=connected_account.id, 5 tool_input={} 6 ) 7 list_id = lists.result[0]["id"] # use the first list, or filter by name 8 9 # Bulk-save up to 1,000 profiles in one call — more efficient than looping 10 actions.execute_tool( 11 tool_name="phantombuster_leads_save_many", 12 connected_account_id=connected_account.id, 13 tool_input={ 14 "listId": list_id, 15 "leads": [ 16 { 17 "firstName": p.get("firstName"), 18 "lastName": p.get("lastName"), 19 "email": p.get("email"), 20 "linkedinUrl": p.get("linkedinUrl"), 21 "company": p.get("company"), 22 "jobTitle": p.get("title"), 23 "additionalFields": { 24 "source": "phantombuster-scraper", 25 "agentId": agent_id, 26 "containerId": container_id, 27 }, 28 } 29 for p in final_result.result 30 ], 31 } 32 ) 33 print(f"{len(final_result.result)} leads saved to list {list_id}.") ``` ### Check resource usage before running agents Avoid quota errors by verifying execution time and credit balances before launching a large scraping run. examples/phantombuster\_check\_resources.py ```python 1 resources = actions.execute_tool( 2 tool_name="phantombuster_org_fetch_resources", 3 connected_account_id=connected_account.id, 4 tool_input={} 5 ) 6 7 exec_time = resources.result.get("executionTime", {}) 8 ai_credits = resources.result.get("aiCredits", {}) 9 10 if exec_time.get("remaining", 0) < 30: 11 raise RuntimeError( 12 f"Insufficient execution time: {exec_time.get('remaining')} min remaining. " 13 "Upgrade at phantombuster.com/pricing or wait for your plan to reset." 14 ) 15 16 print(f"Execution time: {exec_time['remaining']} min remaining ({exec_time.get('used')} used)") 17 print(f"AI credits: {ai_credits.get('remaining', 'N/A')}") ``` ### Run AI completions on scraped data Use PhantomBuster’s AI service to extract structured data from raw agent output — such as parsing job titles into seniority levels or extracting skills from profile summaries. Requires AI credits `phantombuster_ai_completions` consumes AI credits from your plan. Monitor usage at **PhantomBuster dashboard → Usage**. AI credits are included in Pro+ plans and available as an add-on. examples/phantombuster\_ai\_enrichment.py ```python 1 # Extract structured data from a raw LinkedIn profile headline 2 completion = actions.execute_tool( 3 tool_name="phantombuster_ai_completions", 4 connected_account_id=connected_account.id, 5 tool_input={ 6 "model": "gpt-4o", 7 "messages": [ 8 { 9 "role": "system", 10 "content": "Extract the seniority level and primary skill from this LinkedIn headline. Return JSON only.", 11 }, 12 { 13 "role": "user", 14 "content": "Senior Software Engineer at Acme Corp | React, TypeScript, GraphQL", 15 }, 16 ], 17 "responseSchema": { 18 "type": "object", 19 "properties": { 20 "seniority": {"type": "string", "enum": ["junior", "mid", "senior", "lead", "exec"]}, 21 "primarySkill": {"type": "string"}, 22 }, 23 "required": ["seniority", "primarySkill"], 24 }, 25 } 26 ) 27 print("Parsed profile:", completion.result) 28 # → {'seniority': 'senior', 'primarySkill': 'React'} ``` ### LangChain integration Let an LLM decide which PhantomBuster tool to call based on natural language. This example builds an agent that can manage automation runs and leads in response to user input. examples/phantombuster\_langchain.py ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 from langchain_openai import ChatOpenAI 4 from langchain.agents import AgentExecutor, create_openai_tools_agent 5 from langchain_core.prompts import ( 6 ChatPromptTemplate, SystemMessagePromptTemplate, 7 HumanMessagePromptTemplate, MessagesPlaceholder, PromptTemplate 8 ) 9 load_dotenv() 10 11 scalekit_client = scalekit.client.ScalekitClient( 12 client_id=os.getenv("SCALEKIT_CLIENT_ID"), 13 client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), 14 env_url=os.getenv("SCALEKIT_ENV_URL"), 15 ) 16 actions = scalekit_client.actions 17 18 identifier = "user_123" 19 20 # Resolve connected account (API key auth — no OAuth redirect needed) 21 actions.get_or_create_connected_account( 22 connection_name="phantombuster", 23 identifier=identifier 24 ) 25 26 # Load all PhantomBuster tools in LangChain format 27 tools = actions.langchain.get_tools( 28 identifier=identifier, 29 providers=["PHANTOMBUSTER"], 30 page_size=100 31 ) 32 33 prompt = ChatPromptTemplate.from_messages([ 34 SystemMessagePromptTemplate(prompt=PromptTemplate( 35 input_variables=[], 36 template=( 37 "You are a PhantomBuster automation assistant. " 38 "Use the available tools to manage agents, check resource usage, " 39 "manage leads, and analyse automation run results." 40 ) 41 )), 42 MessagesPlaceholder(variable_name="chat_history", optional=True), 43 HumanMessagePromptTemplate(prompt=PromptTemplate( 44 input_variables=["input"], template="{input}" 45 )), 46 MessagesPlaceholder(variable_name="agent_scratchpad") 47 ]) 48 49 llm = ChatOpenAI(model="gpt-4o") 50 agent = create_openai_tools_agent(llm, tools, prompt) 51 agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) 52 53 result = agent_executor.invoke({ 54 "input": "List all my agents, show which ones ran in the last 24 hours, and tell me how many leads are in each list" 55 }) 56 print(result["output"]) ``` ## Tool list [Section titled “Tool list”](#tool-list) *** ### Agent management [Section titled “Agent management”](#agent-management) ## `phantombuster_agents_fetch_all` [Section titled “phantombuster\_agents\_fetch\_all”](#phantombuster_agents_fetch_all) Retrieve all automation agents in the PhantomBuster organization. Returns agent IDs, names, associated scripts, schedules, and current status. Use this to build an inventory of available agents before launching or managing them. **Required plan**: All plans. *This tool takes no input parameters.* ## `phantombuster_agent_fetch` [Section titled “phantombuster\_agent\_fetch”](#phantombuster_agent_fetch) Retrieve full details of a specific PhantomBuster agent by its ID — including name, script, schedule, launch type, argument configuration, and current status. **Required plan**: All plans. | Name | Type | Required | Description | | ---- | ------ | -------- | -------------------------------- | | `id` | string | Yes | The ID of the agent to retrieve. | ## `phantombuster_agent_save` [Section titled “phantombuster\_agent\_save”](#phantombuster_agent_save) Create a new PhantomBuster agent or update an existing one. Supports configuring the script, schedule, proxy, notifications, execution limits, and launch arguments. Pass an `id` to update an existing agent; omit `id` to create a new one. **Required plan**: All plans. Custom scripts require **Team+**. | Name | Type | Required | Description | | -------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------- | | `id` | string | No | Agent ID to update. Omit to create a new agent. | | `name` | string | No | Human-readable name for the agent. | | `scriptId` | string | No | ID of the script this agent runs. Use `phantombuster_scripts_fetch_all` to find script IDs. | | `schedule` | string | No | Cron-style schedule string (e.g., `0 9 * * 1-5` for weekdays at 9 AM). Set to `null` to disable. | | `launchType` | string | No | How the agent is triggered: `manually`, `automatically`, or `repeatedly`. | | `arguments` | object | No | Key-value pairs of input arguments the script expects. Must match the script’s argument schema. | | `executionTimeLimit` | integer | No | Maximum execution time in minutes before the agent is force-stopped. | | `fileSizeLimit` | integer | No | Maximum output file size in MB. | | `proxy` | string | No | Proxy configuration: `none`, `datacenter`, or `residential`. | | `notifications` | object | No | Notification settings including `slackWebhookUrl` and notification triggers (`onSuccess`, `onError`). | ## `phantombuster_agent_delete` [Section titled “phantombuster\_agent\_delete”](#phantombuster_agent_delete) Permanently delete a PhantomBuster agent and all its associated data including execution history and output files. **This action is irreversible.** **Required plan**: All plans. | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | The ID of the agent to permanently delete. | ## `phantombuster_agent_launch` [Section titled “phantombuster\_agent\_launch”](#phantombuster_agent_launch) Launch a PhantomBuster automation agent asynchronously. Starts execution immediately and returns a `containerId` to track progress. Use `phantombuster_container_fetch_output` or `phantombuster_container_fetch_result` to retrieve results. **Required plan**: All plans (execution time consumed from plan quota). | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------------------------------------------- | | `id` | string | Yes | The ID of the agent to launch. | | `arguments` | object | No | Override launch arguments for this run. Must match the script’s argument schema. | | `output` | string | No | Output format: `result-object` or `file-manager`. | ## `phantombuster_agent_launch_soon` [Section titled “phantombuster\_agent\_launch\_soon”](#phantombuster_agent_launch_soon) Schedule a PhantomBuster agent to launch within a specified number of minutes. Useful for delayed execution without setting up a full recurring schedule. **Required plan**: All plans. | Name | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------- | | `id` | string | Yes | The ID of the agent to schedule. | | `minutes` | integer | Yes | Number of minutes from now to launch the agent (minimum 1). | ## `phantombuster_agent_stop` [Section titled “phantombuster\_agent\_stop”](#phantombuster_agent_stop) Stop a currently running PhantomBuster agent execution. Gracefully halts the agent and saves any partial results collected up to that point. **Required plan**: All plans. | Name | Type | Required | Description | | ---- | ------ | -------- | ---------------------------------------------------- | | `id` | string | Yes | The ID of the agent whose current execution to stop. | ## `phantombuster_agents_fetch_deleted` [Section titled “phantombuster\_agents\_fetch\_deleted”](#phantombuster_agents_fetch_deleted) Retrieve all deleted agents in the PhantomBuster organization. Returns agent IDs, names, creation timestamps, deletion timestamps, and who deleted each agent. Useful for audit trails and recovery planning. **Required plan**: All plans. *This tool takes no input parameters.* ## `phantombuster_agents_unschedule_all` [Section titled “phantombuster\_agents\_unschedule\_all”](#phantombuster_agents_unschedule_all) Disable automatic launch for **all** agents in the current PhantomBuster organization. Agents remain intact but will only run when launched manually. Use with caution — this affects every scheduled agent in the org. **Required plan**: All plans. *This tool takes no input parameters.* *** ### Container management [Section titled “Container management”](#container-management) ## `phantombuster_containers_fetch_all` [Section titled “phantombuster\_containers\_fetch\_all”](#phantombuster_containers_fetch_all) Retrieve all execution containers (past runs) for a specific PhantomBuster agent. Returns container IDs, status, launch type, exit codes, timestamps, and runtime events for each execution. **Required plan**: All plans. | Name | Type | Required | Description | | --------- | ------- | -------- | -------------------------------------------------------- | | `agentId` | string | Yes | The ID of the agent whose execution history to retrieve. | | `page` | integer | No | Page number for pagination (default 1). | | `perPage` | integer | No | Number of containers per page (default 20, max 100). | ## `phantombuster_container_fetch` [Section titled “phantombuster\_container\_fetch”](#phantombuster_container_fetch) Retrieve a single PhantomBuster container by its ID. Returns status, timestamps, launch type, exit code, and optionally the full output, result object, and runtime events. **Required plan**: All plans. | Name | Type | Required | Description | | ------------ | ------- | -------- | ----------------------------------------------------------------- | | `id` | string | Yes | The container ID to retrieve. | | `withOutput` | boolean | No | Include the full console output in the response. | | `withResult` | boolean | No | Include the structured result object in the response. | | `withEvents` | boolean | No | Include runtime events (steps, warnings, errors) in the response. | ## `phantombuster_container_fetch_output` [Section titled “phantombuster\_container\_fetch\_output”](#phantombuster_container_fetch_output) Retrieve the console output and execution logs of a specific PhantomBuster container. Useful for monitoring execution progress, debugging errors, and viewing step-by-step agent activity. **Required plan**: All plans. | Name | Type | Required | Description | | --------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Yes | The container ID whose output to retrieve. | | `fromOutputPos` | integer | No | Byte offset to start reading from. Use the `nextOutputPos` value from a previous call to retrieve only new output since the last fetch (incremental polling). | ## `phantombuster_container_fetch_result` [Section titled “phantombuster\_container\_fetch\_result”](#phantombuster_container_fetch_result) Retrieve the final result object of a completed PhantomBuster container. Returns the structured data extracted or produced by the agent — such as scraped profiles, leads, or exported records. **Required plan**: All plans. | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------------------------------ | | `id` | string | Yes | The container ID whose result to retrieve. | ## `phantombuster_container_attach` [Section titled “phantombuster\_container\_attach”](#phantombuster_container_attach) Attach to a running PhantomBuster container and stream its console output in real-time. Returns a live stream of log lines as the agent executes. Use when you need to monitor execution as it happens rather than polling after completion. **Required plan**: All plans. | Name | Type | Required | Description | | ---- | ------ | -------- | --------------------------------------------- | | `id` | string | Yes | The ID of the running container to attach to. | ## `phantombuster_agent_fetch_output` [Section titled “phantombuster\_agent\_fetch\_output”](#phantombuster_agent_fetch_output) Get the output of the most recent container of an agent. Designed for incremental data retrieval — use `fromOutputPos` to fetch only new output since the last call, avoiding re-reading data you have already processed. **Required plan**: All plans. | Name | Type | Required | Description | | --------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `id` | string | Yes | The agent ID whose latest container output to retrieve. | | `fromOutputPos` | integer | No | Byte offset to start reading from. Pass the `nextOutputPos` value returned by a previous call to fetch only new output. | ## `phantombuster_org_fetch_running_containers` [Section titled “phantombuster\_org\_fetch\_running\_containers”](#phantombuster_org_fetch_running_containers) List all currently executing containers across the PhantomBuster organization. Returns container IDs, associated agent IDs and names, creation timestamps, launch types, and script slugs. Use to monitor live execution or detect runaway agents. **Required plan**: All plans. *This tool takes no input parameters.* *** ### Script management [Section titled “Script management”](#script-management) ## `phantombuster_scripts_fetch_all` [Section titled “phantombuster\_scripts\_fetch\_all”](#phantombuster_scripts_fetch_all) Retrieve all scripts associated with the current PhantomBuster user. Returns script IDs, names, slugs, descriptions, branches, and manifest details. Use to discover available scripts before creating or configuring agents. **Required plan**: All plans. *This tool takes no input parameters.* ## `phantombuster_script_fetch` [Section titled “phantombuster\_script\_fetch”](#phantombuster_script_fetch) Retrieve a specific PhantomBuster script by ID, including its manifest, argument schema, and output types. Optionally include the full source code. **Required plan**: All plans. Source code access requires **Team+**. | Name | Type | Required | Description | | ------------ | ------- | -------- | -------------------------------------------------------------------------------- | | `id` | string | Yes | The script ID to retrieve. | | `withSource` | boolean | No | Include the full source code of the script in the response. Requires Team+ plan. | *** ### Branch management [Section titled “Branch management”](#branch-management) ## `phantombuster_branches_fetch_all` [Section titled “phantombuster\_branches\_fetch\_all”](#phantombuster_branches_fetch_all) Retrieve all branches associated with the current PhantomBuster organization. Branches let you manage multiple versions of custom scripts — staging vs. production, for example. **Required plan**: Team+. *This tool takes no input parameters.* ## `phantombuster_branch_create` [Section titled “phantombuster\_branch\_create”](#phantombuster_branch_create) Create a new script branch in the current PhantomBuster organization. Use branches to develop and test script changes without affecting production agents. **Required plan**: Team+. | Name | Type | Required | Description | | ------ | ------ | -------- | ------------------------------------------------------------- | | `name` | string | Yes | A unique name for the new branch (e.g., `staging`, `dev-v2`). | ## `phantombuster_branch_delete` [Section titled “phantombuster\_branch\_delete”](#phantombuster_branch_delete) Permanently delete a branch by ID from the current PhantomBuster organization. All scripts associated with the branch will also be removed. **This action is irreversible.** **Required plan**: Team+. | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------------------------------- | | `id` | string | Yes | The ID of the branch to permanently delete. | ## `phantombuster_branch_release` [Section titled “phantombuster\_branch\_release”](#phantombuster_branch_release) Release (promote to production) specified scripts on a branch in the current PhantomBuster organization. Use this to deploy tested scripts from a staging branch to production. **Required plan**: Team+. | Name | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------------------- | | `id` | string | Yes | The ID of the branch to release scripts from. | | `scripts` | array | Yes | List of script IDs to promote to production from this branch. | *** ### Organization [Section titled “Organization”](#organization) ## `phantombuster_org_fetch` [Section titled “phantombuster\_org\_fetch”](#phantombuster_org_fetch) Retrieve details of the current PhantomBuster organization — including plan, billing information, timezone, proxy configuration, and connected CRM integrations. **Required plan**: All plans. *This tool takes no input parameters.* ## `phantombuster_org_fetch_resources` [Section titled “phantombuster\_org\_fetch\_resources”](#phantombuster_org_fetch_resources) Retrieve the current PhantomBuster organization’s resource usage and limits. Returns daily and monthly usage for execution time, mail, captcha, AI credits, SERP credits, storage, and agent count. Use to proactively manage quota before hitting limits. **Required plan**: All plans. *This tool takes no input parameters.* ## `phantombuster_org_fetch_agent_groups` [Section titled “phantombuster\_org\_fetch\_agent\_groups”](#phantombuster_org_fetch_agent_groups) Retrieve the agent groups and their ordering for the current PhantomBuster organization. Agent groups are used to organise agents in the dashboard. **Required plan**: All plans. *This tool takes no input parameters.* ## `phantombuster_org_save_agent_groups` [Section titled “phantombuster\_org\_save\_agent\_groups”](#phantombuster_org_save_agent_groups) Update the agent groups and their ordering for the current PhantomBuster organization. The order of groups and agents within groups is preserved exactly as provided. **Required plan**: All plans. | Name | Type | Required | Description | | -------- | ----- | -------- | ------------------------------------------------------------------------------------------------------------------- | | `groups` | array | Yes | Ordered array of group objects. Each group must include `name` (string) and `agentIds` (array of agent ID strings). | ## `phantombuster_org_export_agent_usage` [Section titled “phantombuster\_org\_export\_agent\_usage”](#phantombuster_org_export_agent_usage) Export a CSV file containing agent usage metrics for the current PhantomBuster organization over a specified number of days (maximum 6 months / 180 days). **Required plan**: Pro+ (full 6-month range). Starter plan limited to 30 days. | Name | Type | Required | Description | | ------ | ------- | -------- | ----------------------------------------------------- | | `days` | integer | Yes | Number of past days to include in the export (1–180). | ## `phantombuster_org_export_container_usage` [Section titled “phantombuster\_org\_export\_container\_usage”](#phantombuster_org_export_container_usage) Export a CSV file containing container (agent run) usage metrics for the current PhantomBuster organization. Optionally filter to a specific agent to analyse a single automation’s resource consumption. **Required plan**: Pro+ (full history). Starter plan limited to 30 days. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------------------------------- | | `agentId` | string | No | Filter the export to a specific agent ID. Omit to export all agents. | ## `phantombuster_org_save_crm_contact` [Section titled “phantombuster\_org\_save\_crm\_contact”](#phantombuster_org_save_crm_contact) Save a new contact to the organization’s connected CRM (HubSpot). Requires a HubSpot CRM integration to be configured in **PhantomBuster dashboard → Settings → CRM**. **Required plan**: Pro+. HubSpot integration must be connected first. CRM integration required This tool fails with a `403` error if no CRM is connected. Set up the HubSpot integration at [phantombuster.com](https://phantombuster.com) → **Settings** → **CRM** before calling this tool. | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------------------------ | | `firstName` | string | No | Contact’s first name. | | `lastName` | string | No | Contact’s last name. | | `email` | string | No | Contact’s email address. Used as the unique identifier in HubSpot. | | `phone` | string | No | Contact’s phone number. | | `company` | string | No | Company name associated with the contact. | | `jobTitle` | string | No | Contact’s job title. | | `linkedinUrl` | string | No | Contact’s LinkedIn profile URL. | | `website` | string | No | Contact’s or company’s website URL. | *** ### Lead management [Section titled “Lead management”](#lead-management) ## `phantombuster_leads_save` [Section titled “phantombuster\_leads\_save”](#phantombuster_leads_save) Save a single lead to PhantomBuster organization storage. If a lead with the same identifier already exists, it is updated. **Required plan**: Starter+. | Name | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------------------------------------------------------------------------- | | `listId` | string | Yes | The ID of the lead list to save the lead into. | | `firstName` | string | No | Lead’s first name. | | `lastName` | string | No | Lead’s last name. | | `email` | string | No | Lead’s email address. | | `linkedinUrl` | string | No | Lead’s LinkedIn profile URL. Used as a unique identifier if present. | | `company` | string | No | Company the lead is associated with. | | `jobTitle` | string | No | Lead’s job title. | | `additionalFields` | object | No | Any extra key-value pairs to store with the lead (e.g., `{"source": "webinar", "score": 90}`). | ## `phantombuster_leads_save_many` [Section titled “phantombuster\_leads\_save\_many”](#phantombuster_leads_save_many) Save multiple leads at once to PhantomBuster organization storage. Existing leads with matching identifiers are updated. More efficient than calling `phantombuster_leads_save` in a loop for bulk operations. **Required plan**: Starter+. | Name | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `listId` | string | Yes | The ID of the lead list to save leads into. | | `leads` | array | Yes | Array of lead objects. Each object supports the same fields as `phantombuster_leads_save` (`firstName`, `lastName`, `email`, `linkedinUrl`, `company`, `jobTitle`, `additionalFields`). Maximum 1,000 leads per call. | ## `phantombuster_leads_delete_many` [Section titled “phantombuster\_leads\_delete\_many”](#phantombuster_leads_delete_many) Permanently delete multiple leads from PhantomBuster organization storage by their IDs. **This action is irreversible.** **Required plan**: Starter+. | Name | Type | Required | Description | | ----- | ----- | -------- | -------------------------------------------------------------------- | | `ids` | array | Yes | Array of lead IDs to permanently delete. Maximum 1,000 IDs per call. | ## `phantombuster_leads_fetch_by_list` [Section titled “phantombuster\_leads\_fetch\_by\_list”](#phantombuster_leads_fetch_by_list) Fetch paginated leads belonging to a specific lead list in PhantomBuster organization storage. Use `page` and `perPage` to iterate through large lists. **Required plan**: Starter+. | Name | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------- | | `listId` | string | Yes | The ID of the lead list to fetch leads from. | | `page` | integer | No | Page number for pagination (default 1). | | `perPage` | integer | No | Number of leads per page (default 20, max 1,000). | *** ### Lead lists [Section titled “Lead lists”](#lead-lists) ## `phantombuster_lists_fetch_all` [Section titled “phantombuster\_lists\_fetch\_all”](#phantombuster_lists_fetch_all) Retrieve all lead lists in the PhantomBuster organization’s storage. Returns list IDs, names, lead counts, and creation timestamps. Use to discover available lists before fetching or saving leads. **Required plan**: Starter+. *This tool takes no input parameters.* ## `phantombuster_list_fetch` [Section titled “phantombuster\_list\_fetch”](#phantombuster_list_fetch) Retrieve a specific lead list from PhantomBuster organization storage by its ID. Returns the list metadata including name, lead count, and creation timestamp. **Required plan**: Starter+. | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------------------------ | | `id` | string | Yes | The ID of the lead list to retrieve. | ## `phantombuster_list_delete` [Section titled “phantombuster\_list\_delete”](#phantombuster_list_delete) Permanently delete a lead list from PhantomBuster organization storage by its ID. All leads within the list are also deleted. **This action is irreversible.** **Required plan**: Starter+. | Name | Type | Required | Description | | ---- | ------ | -------- | ---------------------------------------------- | | `id` | string | Yes | The ID of the lead list to permanently delete. | *** ### AI and utilities [Section titled “AI and utilities”](#ai-and-utilities) ## `phantombuster_ai_completions` [Section titled “phantombuster\_ai\_completions”](#phantombuster_ai_completions) Get an AI text completion from PhantomBuster’s AI service. Supports multiple models including GPT-4o and GPT-4.1-mini. Optionally request structured JSON output via a response schema — useful for extracting structured data from unstructured agent output. **Required plan**: Pro+. Each call consumes **AI credits** from your plan quota. Monitor credit usage at **Dashboard → Usage**. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | string | Yes | Model to use: `gpt-4o` (highest quality) or `gpt-4.1-mini` (faster, lower credit cost). | | `messages` | array | Yes | Conversation messages in OpenAI format: `[{"role": "user", "content": "..."}]`. Supported roles: `system`, `user`, `assistant`. | | `responseSchema` | object | No | JSON Schema object defining the expected response structure. When provided, the model returns structured JSON conforming to the schema instead of free-form text. | ## `phantombuster_location_ip` [Section titled “phantombuster\_location\_ip”](#phantombuster_location_ip) Retrieve the country associated with an IPv4 or IPv6 address using PhantomBuster’s geolocation service. Useful for validating proxy locations or enriching lead data with geographic context. **Required plan**: All plans. | Name | Type | Required | Description | | ---- | ------ | -------- | ---------------------------------------------------------------------------------- | | `ip` | string | Yes | The IPv4 or IPv6 address to geolocate (e.g., `8.8.8.8` or `2001:4860:4860::8888`). | --- # DOCUMENT BOUNDARY --- # Pipedrive > Connect to Pipedrive CRM. Manage deals, persons, organizations, leads, activities, and sales pipelines with 68 tools. Connect to Pipedrive CRM. Manage deals, persons, organizations, leads, activities, and sales pipelines. ![Pipedrive logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/pipedrive.svg) Supports authentication: OAuth 2.0 What you can build with this connector | Use case | Tools involved | | -------------------------------------- | ----------------------------------------------------------------------------------------------- | | **Automated deal creation from leads** | `pipedrive_lead_get` → `pipedrive_deal_create` → `pipedrive_activity_create` | | **Contact enrichment pipeline** | `pipedrive_persons_search` → `pipedrive_person_update` + `pipedrive_note_create` | | **Pipeline health monitoring** | `pipedrive_deals_list` (by stage) → `pipedrive_deal_update` (move stage) | | **Activity scheduling agent** | `pipedrive_deal_get` → `pipedrive_activity_create` (follow-up call/meeting) | | **Revenue forecasting** | `pipedrive_pipelines_list` → `pipedrive_deals_list` → aggregate deal values | | **Competitive account mapping** | `pipedrive_organizations_search` → `pipedrive_organization_deals_list` → `pipedrive_notes_list` | | **Product attach rate analysis** | `pipedrive_deal_products_list` → `pipedrive_products_list` → compute metrics | | **Onboarding automation** | `pipedrive_person_create` → `pipedrive_deal_create` → `pipedrive_webhook_create` | **Key concepts:** * **Deals vs Leads**: Leads are unqualified opportunities without a pipeline stage. Convert a lead to a deal with `pipedrive_deal_create` once it qualifies. * **Persons and organizations**: A person can belong to one organization. Linking both to a deal gives full account context. * **Stages and pipelines**: Every deal must be in a stage, which belongs to a pipeline. Use `pipedrive_stages_list` to get valid stage IDs before creating deals. * **Activity types**: Valid types include `call`, `meeting`, `email`, `lunch`, `deadline`, `task`, and `other`. Fetch the full list with `pipedrive_activity_types_list`. * **Pagination**: List endpoints use `start` (offset) and `limit`. The default limit is 100; max is 500 for most endpoints. * **Filters**: Use `pipedrive_filter_create` to save complex queries and pass `filter_id` to list endpoints for fast, reusable filtering. ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Pipedrive connector so Scalekit handles the OAuth flow and token lifecycle on your behalf. The connection name you create is used to identify and invoke the connection in code. 1. ### Create a connection in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Pipedrive** and click **Create**. * Click **Use your own credentials** and copy the **Redirect URI**. It looks like: `https:///sso/v1/oauth//callback` Keep this tab open — you’ll return to it in step 3. 2. ### Create a Pipedrive OAuth app * Go to the [Pipedrive Developer Hub](https://developers.pipedrive.com/) and sign in with your Pipedrive account. * Create a new app and fill in the form: * **App name** — a name to identify your app (e.g., `My Sales Agent`) * **Callback URL** — paste the Redirect URI you copied from Scalekit * Under **OAuth & Access Scopes**, select the permissions your agent needs: | Scope | Access granted | | ----------------- | ---------------------------------------- | | `deals:full` | Read and write deals | | `contacts:full` | Read and write persons and organizations | | `leads:full` | Read and write leads | | `activities:full` | Read and write activities | | `products:full` | Read and write products | | `users:read` | Read user information | | `webhooks:full` | Manage webhooks | * Click **Save**. ![](/.netlify/images?url=_astro%2Fcreate-oauth-app.DwUJGJ-B.png\&w=960\&h=620\&dpl=69cce21a4f77360008b1503a) Use the minimum scopes Request only the scopes your agent actually uses. Narrow scopes reduce the blast radius if credentials are compromised and make it easier for users to consent. 3. ### Copy your client credentials After saving your app, Pipedrive shows the **Client ID** and **Client Secret**. Copy both values now — you will need them in the next step. 4. ### Add credentials in Scalekit * Return to [Scalekit dashboard](https://app.scalekit.com) → **Agent Auth** → **Connections** and open the connection you created in step 1. * Enter the following: * **Client ID** — from Pipedrive * **Client Secret** — from Pipedrive * **Permissions** — the same scopes you selected in Pipedrive * Click **Save**. ![](/.netlify/images?url=_astro%2Fadd-connection.DXw0BbT9.png\&w=948\&h=520\&dpl=69cce21a4f77360008b1503a) Connection name is your identifier The connection name you set here (e.g., `pipedrive`) is the string you pass to `connection_name` in every SDK call. It must match exactly — including case. ## Usage [Section titled “Usage”](#usage) Connect a user’s Pipedrive account and make API calls on their behalf — Scalekit handles OAuth and token refresh automatically. * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'pipedrive'; // connection name from Scalekit dashboard 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get credentials from app.scalekit.com → Developers → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user — send this link to your user 16 const { link } = await actions.getAuthorizationLink({ connectionName, identifier }); 17 console.log('🔗 Authorize Pipedrive:', link); 18 19 // After the user authorizes, make API calls via Scalekit proxy 20 const result = await actions.request({ 21 connectionName, 22 identifier, 23 path: '/v1/deals', 24 method: 'GET', 25 params: { status: 'open', limit: 50 }, 26 }); 27 console.log(result.data.data); // Array of deal objects ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "pipedrive" # connection name from Scalekit dashboard 6 identifier = "user_123" # your unique user identifier 7 8 # Get credentials from app.scalekit.com → Developers → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user — send this link to your user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 print("🔗 Authorize Pipedrive:", link_response.link) 22 23 # After the user authorizes, make API calls via Scalekit proxy 24 result = actions.request( 25 connection_name=connection_name, 26 identifier=identifier, 27 path="/v1/deals", 28 method="GET", 29 params={"status": "open", "limit": 50}, 30 ) 31 print(result["data"]["data"]) # List of deal objects ``` Pipedrive API base URL The Pipedrive REST API base URL is `https://api.pipedrive.com`. Scalekit prefixes it automatically — pass only the path (e.g., `/v1/deals`). ![Scalekit Connected Accounts tab showing authorized Pipedrive users with status, token expiry, and date added columns](/.netlify/images?url=_astro%2Fadd-connected-account.B6et1h_-.png\&w=1000\&h=420\&dpl=69cce21a4f77360008b1503a) ### Scalekit tools [Section titled “Scalekit tools”](#scalekit-tools) Use `actions.execute_tool()` to call any Pipedrive tool by name. Scalekit resolves credentials, calls the Pipedrive API, and returns a structured response. All tool names map directly to the [Tool list](#tool-list) below. **Create a deal and schedule a follow-up call:** * Node.js ```typescript 1 // Create a new deal linked to a person and organization 2 const deal = await actions.executeTool({ 3 connectionName: 'pipedrive', 4 identifier: 'user_123', 5 toolName: 'pipedrive_deal_create', 6 toolInput: { 7 title: 'Acme Corp — Enterprise Plan', 8 value: 48000, 9 currency: 'USD', 10 person_id: 1042, 11 org_id: 305, 12 stage_id: 2, 13 expected_close_date: '2026-06-30', 14 }, 15 }); 16 const dealId = deal.data.id; 17 console.log(`Created deal ${dealId}`); 18 19 // Schedule a discovery call for the new deal 20 const activity = await actions.executeTool({ 21 connectionName: 'pipedrive', 22 identifier: 'user_123', 23 toolName: 'pipedrive_activity_create', 24 toolInput: { 25 subject: 'Discovery call — Acme Corp', 26 type: 'call', 27 deal_id: dealId, 28 due_date: '2026-04-01', 29 due_time: '10:00', 30 duration: '00:30', 31 note: 'Confirm decision-makers and budget approval process', 32 }, 33 }); 34 console.log(`Scheduled activity ${activity.data.id}`); ``` * Python ```python 1 # Create a new deal linked to a person and organization 2 deal = actions.execute_tool( 3 connection_name="pipedrive", 4 identifier="user_123", 5 tool_name="pipedrive_deal_create", 6 tool_input={ 7 "title": "Acme Corp — Enterprise Plan", 8 "value": 48000, 9 "currency": "USD", 10 "person_id": 1042, 11 "org_id": 305, 12 "stage_id": 2, 13 "expected_close_date": "2026-06-30", 14 }, 15 ) 16 deal_id = deal["data"]["id"] 17 print(f"Created deal {deal_id}") 18 19 # Schedule a discovery call for the new deal 20 activity = actions.execute_tool( 21 connection_name="pipedrive", 22 identifier="user_123", 23 tool_name="pipedrive_activity_create", 24 tool_input={ 25 "subject": "Discovery call — Acme Corp", 26 "type": "call", 27 "deal_id": deal_id, 28 "due_date": "2026-04-01", 29 "due_time": "10:00", 30 "duration": "00:30", 31 "note": "Confirm decision-makers and budget approval process", 32 }, 33 ) 34 print(f"Scheduled activity {activity['data']['id']}") ``` **Search for a person and attach a note:** * Node.js ```typescript 1 // Find an existing contact by name 2 const results = await actions.executeTool({ 3 connectionName: 'pipedrive', 4 identifier: 'user_123', 5 toolName: 'pipedrive_persons_search', 6 toolInput: { term: 'Jane Smith', exact_match: false, limit: 5 }, 7 }); 8 9 const persons = results.data.items; 10 if (persons.length > 0) { 11 const personId = persons[0].item.id; 12 13 // Attach a contextual note 14 await actions.executeTool({ 15 connectionName: 'pipedrive', 16 identifier: 'user_123', 17 toolName: 'pipedrive_note_create', 18 toolInput: { 19 content: 'Spoke with Jane re: renewal. She confirmed budget approval in Q2.', 20 person_id: personId, 21 pinned_to_person_flag: true, 22 }, 23 }); 24 console.log(`Note added to person ${personId}`); 25 } ``` * Python ```python 1 # Find an existing contact by name 2 results = actions.execute_tool( 3 connection_name="pipedrive", 4 identifier="user_123", 5 tool_name="pipedrive_persons_search", 6 tool_input={"term": "Jane Smith", "exact_match": False, "limit": 5}, 7 ) 8 9 persons = results["data"]["items"] 10 if persons: 11 person_id = persons[0]["item"]["id"] 12 13 # Attach a contextual note 14 actions.execute_tool( 15 connection_name="pipedrive", 16 identifier="user_123", 17 tool_name="pipedrive_note_create", 18 tool_input={ 19 "content": "Spoke with Jane re: renewal. She confirmed budget approval in Q2.", 20 "person_id": person_id, 21 "pinned_to_person_flag": True, 22 }, 23 ) 24 print(f"Note added to person {person_id}") ``` **Move a deal to the next stage:** * Node.js ```typescript 1 // Fetch current deal details 2 const deal = await actions.executeTool({ 3 connectionName: 'pipedrive', 4 identifier: 'user_123', 5 toolName: 'pipedrive_deal_get', 6 toolInput: { deal_id: 9871 }, 7 }); 8 const currentStage: number = deal.data.stage_id; 9 10 // Fetch all stages in the pipeline to find the next one 11 const stages = await actions.executeTool({ 12 connectionName: 'pipedrive', 13 identifier: 'user_123', 14 toolName: 'pipedrive_stages_list', 15 toolInput: { pipeline_id: deal.data.pipeline_id }, 16 }); 17 const stageIds: number[] = stages.data.map((s: { id: number }) => s.id).sort((a: number, b: number) => a - b); 18 const currentIdx = stageIds.indexOf(currentStage); 19 if (currentIdx >= stageIds.length - 1) { 20 throw new Error('Deal is already in the last stage'); 21 } 22 const nextStage = stageIds[currentIdx + 1]; 23 24 // Advance the deal 25 await actions.executeTool({ 26 connectionName: 'pipedrive', 27 identifier: 'user_123', 28 toolName: 'pipedrive_deal_update', 29 toolInput: { deal_id: 9871, stage_id: nextStage }, 30 }); 31 console.log(`Deal moved from stage ${currentStage} to ${nextStage}`); ``` * Python ```python 1 # Fetch current deal details 2 deal = actions.execute_tool( 3 connection_name="pipedrive", 4 identifier="user_123", 5 tool_name="pipedrive_deal_get", 6 tool_input={"deal_id": 9871}, 7 ) 8 current_stage = deal["data"]["stage_id"] 9 10 # Fetch all stages in the pipeline to find the next one 11 stages = actions.execute_tool( 12 connection_name="pipedrive", 13 identifier="user_123", 14 tool_name="pipedrive_stages_list", 15 tool_input={"pipeline_id": deal["data"]["pipeline_id"]}, 16 ) 17 stage_ids = sorted(s["id"] for s in stages["data"]) 18 current_idx = stage_ids.index(current_stage) 19 if current_idx >= len(stage_ids) - 1: 20 raise ValueError("Deal is already in the last stage") 21 next_stage = stage_ids[current_idx + 1] 22 23 # Advance the deal 24 actions.execute_tool( 25 connection_name="pipedrive", 26 identifier="user_123", 27 tool_name="pipedrive_deal_update", 28 tool_input={"deal_id": 9871, "stage_id": next_stage}, 29 ) 30 print(f"Deal moved from stage {current_stage} to {next_stage}") ``` **List all activities due today:** * Node.js ```typescript 1 const today = new Date().toISOString().split('T')[0]; 2 3 const activities = await actions.executeTool({ 4 connectionName: 'pipedrive', 5 identifier: 'user_123', 6 toolName: 'pipedrive_activities_list', 7 toolInput: { start_date: today, end_date: today, done: 0, limit: 100 }, 8 }); 9 10 for (const act of activities.data.data ?? []) { 11 console.log(`[${act.type}] ${act.subject} — due ${act.due_time}`); 12 } ``` * Python ```python 1 from datetime import date 2 3 today = date.today().isoformat() 4 5 activities = actions.execute_tool( 6 connection_name="pipedrive", 7 identifier="user_123", 8 tool_name="pipedrive_activities_list", 9 tool_input={ 10 "start_date": today, 11 "end_date": today, 12 "done": False, 13 "limit": 100, 14 }, 15 ) 16 17 for act in activities["data"]["data"] or []: 18 print(f"[{act['type']}] {act['subject']} — due {act['due_time']}") ``` **Bulk-update stalled deals:** * Node.js ```typescript 1 // Find all open deals that have not moved in 30+ days 2 const stalled = await actions.executeTool({ 3 connectionName: 'pipedrive', 4 identifier: 'user_123', 5 toolName: 'pipedrive_deals_list', 6 toolInput: { status: 'open', limit: 200 }, 7 }); 8 9 const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); 10 for (const deal of stalled.data.data ?? []) { 11 if (deal.last_activity_date && new Date(deal.last_activity_date) < cutoff) { 12 await actions.executeTool({ 13 connectionName: 'pipedrive', 14 identifier: 'user_123', 15 toolName: 'pipedrive_deal_update', 16 toolInput: { deal_id: deal.id, status: 'lost' }, 17 }); 18 console.log(`Marked deal ${deal.id} (${deal.title}) as lost`); 19 } 20 } ``` * Python ```python 1 # Find all open deals that have not moved in 30+ days 2 stalled = actions.execute_tool( 3 connection_name="pipedrive", 4 identifier="user_123", 5 tool_name="pipedrive_deals_list", 6 tool_input={"status": "open", "limit": 200}, 7 ) 8 9 from datetime import datetime, timedelta 10 11 cutoff = datetime.now() - timedelta(days=30) 12 for deal in stalled["data"]["data"] or []: 13 last_activity = deal.get("last_activity_date") 14 if last_activity and datetime.fromisoformat(last_activity) < cutoff: 15 actions.execute_tool( 16 connection_name="pipedrive", 17 identifier="user_123", 18 tool_name="pipedrive_deal_update", 19 tool_input={"deal_id": deal["id"], "status": "lost"}, 20 ) 21 print(f"Marked deal {deal['id']} ({deal['title']}) as lost") ``` ### LangChain integration [Section titled “LangChain integration”](#langchain-integration) Load all 68 Pipedrive tools as LangChain-compatible tools and let an LLM drive the CRM automatically. ```python 1 import logging 2 import os 3 import time 4 5 from langchain_openai import ChatOpenAI 6 from langchain.agents import AgentExecutor, create_tool_calling_agent 7 from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder 8 from scalekit import ScalekitClient 9 10 logging.basicConfig(level=logging.INFO) 11 logger = logging.getLogger(__name__) 12 13 scalekit_client = ScalekitClient( 14 client_id=os.environ["SCALEKIT_CLIENT_ID"], 15 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 16 environment_url=os.environ["SCALEKIT_ENV_URL"], 17 ) 18 19 # Load Pipedrive tools for the user — retry on transient failures 20 MAX_RETRIES = 3 21 tools = None 22 for attempt in range(1, MAX_RETRIES + 1): 23 try: 24 tools = scalekit_client.actions.langchain.get_tools( 25 connection_name="pipedrive", 26 identifier="user_123", 27 ) 28 logger.info("Loaded %d Pipedrive tools", len(tools)) 29 break 30 except Exception as exc: 31 logger.warning("get_tools attempt %d/%d failed: %s", attempt, MAX_RETRIES, exc) 32 if attempt == MAX_RETRIES: 33 raise RuntimeError("Failed to load Pipedrive tools after retries") from exc 34 time.sleep(2 ** attempt) 35 36 llm = ChatOpenAI(model="gpt-4o", temperature=0) 37 38 prompt = ChatPromptTemplate.from_messages([ 39 ("system", "You are a sales operations assistant with full access to Pipedrive CRM. " 40 "Help the user manage their pipeline, contacts, and activities accurately."), 41 MessagesPlaceholder("chat_history", optional=True), 42 ("human", "{input}"), 43 MessagesPlaceholder("agent_scratchpad"), 44 ]) 45 46 agent = create_tool_calling_agent(llm, tools, prompt) 47 agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) 48 49 try: 50 result = agent_executor.invoke({ 51 "input": "Find all open deals worth more than $10,000 and schedule a follow-up call for each one due this Friday." 52 }) 53 print(result["output"]) 54 except Exception as exc: 55 logger.error("Agent invocation failed: %s", exc) 56 raise ``` ## Tool list [Section titled “Tool list”](#tool-list) ### Deals [Section titled “Deals”](#deals) ## `pipedrive_deal_create` [Section titled “pipedrive\_deal\_create”](#pipedrive_deal_create) Create a new deal in Pipedrive CRM. Requires a deal title. Link the deal to a person and organization to give it full contact context. | Name | Type | Required | Description | | --------------------- | ------ | -------- | ------------------------------------------------------------------------- | | `title` | string | Yes | Name of the deal | | `value` | number | No | Monetary value of the deal | | `currency` | string | No | ISO 4217 currency code (e.g., USD, EUR). Defaults to the company currency | | `person_id` | number | No | ID of the person linked to this deal | | `org_id` | number | No | ID of the organization linked to this deal | | `pipeline_id` | number | No | ID of the pipeline. Defaults to the first pipeline | | `stage_id` | number | No | ID of the stage within the pipeline | | `status` | string | No | Deal status: `open`, `won`, `lost`. Defaults to `open` | | `expected_close_date` | string | No | Expected close date in YYYY-MM-DD format | | `probability` | number | No | Win probability as a percentage (0–100) | | `owner_id` | number | No | User ID of the deal owner. Defaults to the authenticated user | | `visible_to` | number | No | Visibility: `1` = owner only, `3` = all users | | `label` | string | No | Deal label | ## `pipedrive_deal_get` [Section titled “pipedrive\_deal\_get”](#pipedrive_deal_get) Retrieve full details of a deal by ID, including linked person, organization, stage, and custom fields. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------- | | `deal_id` | number | Yes | ID of the deal to retrieve | ## `pipedrive_deal_update` [Section titled “pipedrive\_deal\_update”](#pipedrive_deal_update) Update properties of an existing deal. Only the fields you provide are changed — other fields remain unchanged. | Name | Type | Required | Description | | --------------------- | ------ | -------- | ------------------------------------ | | `deal_id` | number | Yes | ID of the deal to update | | `title` | string | No | New deal title | | `value` | number | No | New deal value | | `currency` | string | No | New currency code | | `person_id` | number | No | New linked person ID | | `org_id` | number | No | New linked organization ID | | `pipeline_id` | number | No | New pipeline ID | | `stage_id` | number | No | New stage ID | | `status` | string | No | New status: `open`, `won`, `lost` | | `expected_close_date` | string | No | New expected close date (YYYY-MM-DD) | | `probability` | number | No | New win probability (0–100) | | `owner_id` | number | No | New owner user ID | | `visible_to` | number | No | New visibility setting | ## `pipedrive_deal_delete` [Section titled “pipedrive\_deal\_delete”](#pipedrive_deal_delete) Permanently delete a deal by ID. This action cannot be undone. | Name | Type | Required | Description | | --------- | ------ | -------- | ------------------------ | | `deal_id` | number | Yes | ID of the deal to delete | ## `pipedrive_deals_list` [Section titled “pipedrive\_deals\_list”](#pipedrive_deals_list) List deals with optional filtering by status, stage, pipeline, or owner. Supports pagination. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------------------------------------- | | `filter_id` | number | No | ID of a saved filter to apply | | `status` | string | No | Filter by status: `open`, `won`, `lost`, `deleted`, `all_not_deleted` | | `stage_id` | number | No | Filter by stage ID | | `pipeline_id` | number | No | Filter by pipeline ID | | `user_id` | number | No | Filter by owner user ID | | `start` | number | No | Pagination offset. Defaults to 0 | | `limit` | number | No | Number of results per page. Max 500, defaults to 100 | | `sort` | string | No | Sort field and direction (e.g., `update_time DESC`) | ## `pipedrive_deals_search` [Section titled “pipedrive\_deals\_search”](#pipedrive_deals_search) Search deals by keyword across title, notes, and custom fields. Supports filtering by person, organization, or status. | Name | Type | Required | Description | | ------------- | ------- | -------- | ------------------------------------------------------------------------------------- | | `term` | string | Yes | Search keyword (minimum 2 characters) | | `fields` | string | No | Comma-separated fields to search: `custom_fields`, `notes`, `name`, `label`, `status` | | `exact_match` | boolean | No | When `true`, only exact phrase matches are returned | | `person_id` | number | No | Filter results to deals linked to this person | | `org_id` | number | No | Filter results to deals linked to this organization | | `status` | string | No | Filter by deal status | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page. Max 500 | ## `pipedrive_deal_activities_list` [Section titled “pipedrive\_deal\_activities\_list”](#pipedrive_deal_activities_list) List all activities associated with a deal, optionally filtered by completion status. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------------- | | `deal_id` | number | Yes | ID of the deal | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page | | `done` | number | No | Filter by completion: `0` = incomplete, `1` = done | | `exclude` | string | No | Comma-separated activity IDs to exclude | ## `pipedrive_deal_products_list` [Section titled “pipedrive\_deal\_products\_list”](#pipedrive_deal_products_list) List all products attached to a deal, including unit price, quantity, and discount per product. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------- | | `deal_id` | number | Yes | ID of the deal | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page | ## `pipedrive_deal_participants_list` [Section titled “pipedrive\_deal\_participants\_list”](#pipedrive_deal_participants_list) List all person participants linked to a deal. Participants are contacts associated with a deal beyond the primary contact. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------- | | `deal_id` | number | Yes | ID of the deal | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page | ### Persons [Section titled “Persons”](#persons) ## `pipedrive_person_create` [Section titled “pipedrive\_person\_create”](#pipedrive_person_create) Create a new person (contact) in Pipedrive. A person can have multiple email addresses and phone numbers. | Name | Type | Required | Description | | ------------------ | ------ | -------- | -------------------------------------------------------------------------------- | | `name` | string | Yes | Full name of the person | | `owner_id` | number | No | User ID of the owner. Defaults to authenticated user | | `org_id` | number | No | ID of the organization this person belongs to | | `email` | string | No | Primary email address | | `phone` | string | No | Primary phone number | | `visible_to` | number | No | Visibility: `1` = owner only, `3` = all users | | `marketing_status` | string | No | Marketing consent: `subscribed`, `unsubscribed`, `no_consent`, `never_contacted` | ## `pipedrive_person_get` [Section titled “pipedrive\_person\_get”](#pipedrive_person_get) Retrieve full details of a person by ID, including linked organization, deals, and custom fields. | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------- | | `person_id` | number | Yes | ID of the person to retrieve | ## `pipedrive_person_update` [Section titled “pipedrive\_person\_update”](#pipedrive_person_update) Update properties of an existing person. Only the fields you provide are changed. | Name | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------- | | `person_id` | number | Yes | ID of the person to update | | `name` | string | No | New full name | | `owner_id` | number | No | New owner user ID | | `org_id` | number | No | New organization ID | | `email` | string | No | New primary email address | | `phone` | string | No | New primary phone number | | `visible_to` | number | No | New visibility setting | | `marketing_status` | string | No | New marketing consent status | ## `pipedrive_person_delete` [Section titled “pipedrive\_person\_delete”](#pipedrive_person_delete) Permanently delete a person by ID. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------- | | `person_id` | number | Yes | ID of the person to delete | ## `pipedrive_persons_list` [Section titled “pipedrive\_persons\_list”](#pipedrive_persons_list) List persons with optional filtering by first character or saved filter. Supports pagination. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------- | | `filter_id` | number | No | ID of a saved filter to apply | | `first_char` | string | No | Filter by first character of the person’s name | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page. Max 500, defaults to 100 | | `sort` | string | No | Sort field and direction | ## `pipedrive_persons_search` [Section titled “pipedrive\_persons\_search”](#pipedrive_persons_search) Search persons by keyword across name, email, phone, and custom fields. | Name | Type | Required | Description | | ------------- | ------- | -------- | ------------------------------------------------------------------------------------ | | `term` | string | Yes | Search keyword (minimum 2 characters) | | `fields` | string | No | Comma-separated fields to search: `custom_fields`, `notes`, `name`, `email`, `phone` | | `exact_match` | boolean | No | When `true`, only exact phrase matches are returned | | `org_id` | number | No | Filter to persons belonging to this organization | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page. Max 500 | ### Organizations [Section titled “Organizations”](#organizations) ## `pipedrive_organization_create` [Section titled “pipedrive\_organization\_create”](#pipedrive_organization_create) Create a new organization (company account) in Pipedrive. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------- | | `name` | string | Yes | Organization name | | `owner_id` | number | No | User ID of the owner | | `address` | string | No | Organization address | | `visible_to` | number | No | Visibility: `1` = owner only, `3` = all users | | `label` | string | No | Organization label | ## `pipedrive_organization_get` [Section titled “pipedrive\_organization\_get”](#pipedrive_organization_get) Retrieve full details of an organization by ID, including linked persons and deals. | Name | Type | Required | Description | | -------- | ------ | -------- | ---------------------------------- | | `org_id` | number | Yes | ID of the organization to retrieve | ## `pipedrive_organization_update` [Section titled “pipedrive\_organization\_update”](#pipedrive_organization_update) Update properties of an existing organization. | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------- | | `org_id` | number | Yes | ID of the organization to update | | `name` | string | No | New organization name | | `owner_id` | number | No | New owner user ID | | `address` | string | No | New address | | `visible_to` | number | No | New visibility setting | ## `pipedrive_organization_delete` [Section titled “pipedrive\_organization\_delete”](#pipedrive_organization_delete) Permanently delete an organization by ID. | Name | Type | Required | Description | | -------- | ------ | -------- | -------------------------------- | | `org_id` | number | Yes | ID of the organization to delete | ## `pipedrive_organizations_list` [Section titled “pipedrive\_organizations\_list”](#pipedrive_organizations_list) List organizations with optional filtering. Supports pagination and sorting. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------- | | `filter_id` | number | No | ID of a saved filter to apply | | `first_char` | string | No | Filter by first character of the organization name | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page. Max 500, defaults to 100 | | `sort` | string | No | Sort field and direction | ## `pipedrive_organizations_search` [Section titled “pipedrive\_organizations\_search”](#pipedrive_organizations_search) Search organizations by keyword across name, address, and notes. | Name | Type | Required | Description | | ------------- | ------- | -------- | ----------------------------------------------------------------------------- | | `term` | string | Yes | Search keyword (minimum 2 characters) | | `fields` | string | No | Comma-separated fields to search: `custom_fields`, `notes`, `name`, `address` | | `exact_match` | boolean | No | When `true`, only exact phrase matches are returned | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page | ## `pipedrive_organization_deals_list` [Section titled “pipedrive\_organization\_deals\_list”](#pipedrive_organization_deals_list) List all deals linked to a specific organization. Useful for account-level pipeline views and competitive mapping. | Name | Type | Required | Description | | -------- | ------ | -------- | -------------------------------------------------------------------------- | | `org_id` | number | Yes | ID of the organization | | `status` | string | No | Filter by deal status: `open`, `won`, `lost`, `deleted`, `all_not_deleted` | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page. Max 500, defaults to 100 | | `sort` | string | No | Sort field and direction (e.g., `update_time DESC`) | ## `pipedrive_person_deals_list` [Section titled “pipedrive\_person\_deals\_list”](#pipedrive_person_deals_list) List all deals linked to a specific person. Use this to see all opportunities associated with a contact. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------------------------------------- | | `person_id` | number | Yes | ID of the person | | `status` | string | No | Filter by deal status: `open`, `won`, `lost`, `deleted`, `all_not_deleted` | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page. Max 500, defaults to 100 | | `sort` | string | No | Sort field and direction | ### Leads [Section titled “Leads”](#leads) ## `pipedrive_lead_create` [Section titled “pipedrive\_lead\_create”](#pipedrive_lead_create) Create a new lead in Pipedrive. Leads are unqualified opportunities not yet placed in a pipeline. Convert to a deal with `pipedrive_deal_create` once qualified. | Name | Type | Required | Description | | --------------------- | ------- | -------- | --------------------------------------------------- | | `title` | string | Yes | Title of the lead | | `owner_id` | number | No | User ID of the lead owner | | `label_ids` | array | No | Array of label UUIDs to apply | | `person_id` | number | No | ID of the person linked to this lead | | `org_id` | number | No | ID of the organization linked to this lead | | `value` | object | No | Lead value: `{ "amount": 5000, "currency": "USD" }` | | `expected_close_date` | string | No | Expected close date in YYYY-MM-DD format | | `visible_to` | number | No | Visibility: `1` = owner only, `3` = all users | | `was_seen` | boolean | No | Whether the lead has been seen by the owner | ## `pipedrive_lead_get` [Section titled “pipedrive\_lead\_get”](#pipedrive_lead_get) Retrieve full details of a lead by ID (UUID format). | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------- | | `lead_id` | string | Yes | UUID of the lead to retrieve | ## `pipedrive_lead_update` [Section titled “pipedrive\_lead\_update”](#pipedrive_lead_update) Update properties of an existing lead. | Name | Type | Required | Description | | --------------------- | ------- | -------- | ----------------------------- | | `lead_id` | string | Yes | UUID of the lead to update | | `title` | string | No | New lead title | | `owner_id` | number | No | New owner user ID | | `label_ids` | array | No | New array of label UUIDs | | `person_id` | number | No | New linked person ID | | `org_id` | number | No | New linked organization ID | | `value` | object | No | New lead value object | | `expected_close_date` | string | No | New expected close date | | `is_archived` | boolean | No | Archive or unarchive the lead | | `was_seen` | boolean | No | Mark as seen or unseen | ## `pipedrive_lead_delete` [Section titled “pipedrive\_lead\_delete”](#pipedrive_lead_delete) Permanently delete a lead by ID. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------- | | `lead_id` | string | Yes | UUID of the lead to delete | ## `pipedrive_leads_list` [Section titled “pipedrive\_leads\_list”](#pipedrive_leads_list) List leads with optional filtering by archived status or saved filter. | Name | Type | Required | Description | | ----------------- | ------ | -------- | ----------------------------------------------------------- | | `filter_id` | number | No | ID of a saved filter to apply | | `archived_status` | string | No | Filter by archived state: `archived`, `not_archived`, `all` | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page. Max 500, defaults to 100 | | `sort` | string | No | Sort field and direction | ## `pipedrive_leads_search` [Section titled “pipedrive\_leads\_search”](#pipedrive_leads_search) Search leads by keyword across title, notes, person name, and organization name. | Name | Type | Required | Description | | ------------- | ------- | -------- | ------------------------------------------------------------------- | | `term` | string | Yes | Search keyword (minimum 2 characters) | | `fields` | string | No | Comma-separated fields to search: `custom_fields`, `notes`, `title` | | `exact_match` | boolean | No | When `true`, only exact phrase matches are returned | | `person_id` | number | No | Filter to leads linked to this person | | `org_id` | number | No | Filter to leads linked to this organization | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page | ### Activities [Section titled “Activities”](#activities) ## `pipedrive_activity_create` [Section titled “pipedrive\_activity\_create”](#pipedrive_activity_create) Create a new activity (call, meeting, email, task, etc.) and optionally link it to a deal, person, or lead. | Name | Type | Required | Description | | ----------- | ------- | -------- | ------------------------------------------------------------------------------- | | `subject` | string | Yes | Activity subject or title | | `type` | string | Yes | Activity type: `call`, `meeting`, `email`, `lunch`, `deadline`, `task`, `other` | | `due_date` | string | No | Due date in YYYY-MM-DD format | | `due_time` | string | No | Due time in HH:MM (24-hour) format | | `duration` | string | No | Duration in HH:MM format | | `deal_id` | number | No | ID of the deal to link | | `lead_id` | string | No | UUID of the lead to link | | `person_id` | number | No | ID of the person to link | | `org_id` | number | No | ID of the organization to link | | `note` | string | No | Activity note or description (HTML allowed) | | `location` | string | No | Activity location | | `busy_flag` | boolean | No | Whether the time slot should show as busy | | `done` | boolean | No | Mark the activity as done on creation | | `user_id` | number | No | User ID of the activity owner | ## `pipedrive_activity_get` [Section titled “pipedrive\_activity\_get”](#pipedrive_activity_get) Retrieve full details of an activity by ID. | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------ | | `activity_id` | number | Yes | ID of the activity to retrieve | ## `pipedrive_activity_update` [Section titled “pipedrive\_activity\_update”](#pipedrive_activity_update) Update properties of an existing activity. | Name | Type | Required | Description | | ------------- | ------- | -------- | --------------------------------------------- | | `activity_id` | number | Yes | ID of the activity to update | | `subject` | string | No | New subject | | `type` | string | No | New activity type | | `due_date` | string | No | New due date (YYYY-MM-DD) | | `due_time` | string | No | New due time (HH:MM) | | `duration` | string | No | New duration (HH:MM) | | `deal_id` | number | No | New linked deal ID | | `person_id` | number | No | New linked person ID | | `org_id` | number | No | New linked organization ID | | `note` | string | No | New note content | | `done` | boolean | No | Mark as done (`true`) or incomplete (`false`) | | `busy_flag` | boolean | No | New busy flag | ## `pipedrive_activity_delete` [Section titled “pipedrive\_activity\_delete”](#pipedrive_activity_delete) Permanently delete an activity by ID. | Name | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------- | | `activity_id` | number | Yes | ID of the activity to delete | ## `pipedrive_activities_list` [Section titled “pipedrive\_activities\_list”](#pipedrive_activities_list) List activities with optional filtering by type, user, date range, or completion status. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------- | | `type` | string | No | Filter by activity type (e.g., `call`, `meeting`) | | `user_id` | number | No | Filter by owner user ID | | `filter_id` | number | No | ID of a saved filter to apply | | `start_date` | string | No | Inclusive start date filter (YYYY-MM-DD) | | `end_date` | string | No | Inclusive end date filter (YYYY-MM-DD) | | `done` | number | No | Filter by completion: `0` = incomplete, `1` = done | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page. Max 500, defaults to 100 | ## `pipedrive_activity_types_list` [Section titled “pipedrive\_activity\_types\_list”](#pipedrive_activity_types_list) List all available activity types configured in the Pipedrive account (e.g., call, meeting, email, custom types). This tool takes no parameters. ### Notes [Section titled “Notes”](#notes) ## `pipedrive_note_create` [Section titled “pipedrive\_note\_create”](#pipedrive_note_create) Create a note and attach it to a deal, person, organization, or lead. | Name | Type | Required | Description | | ----------------------- | ------- | -------- | --------------------------------------- | | `content` | string | Yes | Note content (HTML allowed) | | `deal_id` | number | No | ID of the deal to link | | `person_id` | number | No | ID of the person to link | | `org_id` | number | No | ID of the organization to link | | `lead_id` | string | No | UUID of the lead to link | | `user_id` | number | No | User ID of the note author | | `pinned_to_deal_flag` | boolean | No | Pin the note to the linked deal | | `pinned_to_person_flag` | boolean | No | Pin the note to the linked person | | `pinned_to_org_flag` | boolean | No | Pin the note to the linked organization | ## `pipedrive_note_get` [Section titled “pipedrive\_note\_get”](#pipedrive_note_get) Retrieve the content and metadata of a note by ID. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------- | | `note_id` | number | Yes | ID of the note to retrieve | ## `pipedrive_note_update` [Section titled “pipedrive\_note\_update”](#pipedrive_note_update) Update the content or pin status of an existing note. | Name | Type | Required | Description | | ----------------------- | ------- | -------- | --------------------------------------------- | | `note_id` | number | Yes | ID of the note to update | | `content` | string | No | New note content | | `deal_id` | number | No | New linked deal ID | | `person_id` | number | No | New linked person ID | | `org_id` | number | No | New linked organization ID | | `pinned_to_deal_flag` | boolean | No | Update pin status for the linked deal | | `pinned_to_person_flag` | boolean | No | Update pin status for the linked person | | `pinned_to_org_flag` | boolean | No | Update pin status for the linked organization | ## `pipedrive_note_delete` [Section titled “pipedrive\_note\_delete”](#pipedrive_note_delete) Permanently delete a note by ID. | Name | Type | Required | Description | | --------- | ------ | -------- | ------------------------ | | `note_id` | number | Yes | ID of the note to delete | ## `pipedrive_notes_list` [Section titled “pipedrive\_notes\_list”](#pipedrive_notes_list) List notes with optional filtering by linked object (deal, person, organization, or lead). | Name | Type | Required | Description | | ----------------------- | ------- | -------- | -------------------------------------------------- | | `user_id` | number | No | Filter by note author | | `deal_id` | number | No | Filter by linked deal | | `person_id` | number | No | Filter by linked person | | `org_id` | number | No | Filter by linked organization | | `lead_id` | string | No | Filter by linked lead UUID | | `start_date` | string | No | Filter notes created after this date (YYYY-MM-DD) | | `end_date` | string | No | Filter notes created before this date (YYYY-MM-DD) | | `pinned_to_deal_flag` | boolean | No | Filter to notes pinned to deals | | `pinned_to_person_flag` | boolean | No | Filter to notes pinned to persons | | `pinned_to_org_flag` | boolean | No | Filter to notes pinned to organizations | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page | ### Files [Section titled “Files”](#files) ## `pipedrive_file_upload` [Section titled “pipedrive\_file\_upload”](#pipedrive_file_upload) Upload a file and optionally attach it to a deal, person, organization, activity, note, lead, or product. | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------ | | `file` | string | Yes | Base64-encoded file content or a public file URL | | `file_name` | string | No | File name including extension | | `deal_id` | number | No | ID of the deal to attach the file to | | `person_id` | number | No | ID of the person to attach the file to | | `org_id` | number | No | ID of the organization to attach the file to | | `activity_id` | number | No | ID of the activity to attach the file to | | `note_id` | number | No | ID of the note to attach the file to | | `lead_id` | string | No | UUID of the lead to attach the file to | | `product_id` | number | No | ID of the product to attach the file to | ## `pipedrive_file_get` [Section titled “pipedrive\_file\_get”](#pipedrive_file_get) Retrieve metadata and download URL for a file by ID. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------- | | `file_id` | number | Yes | ID of the file to retrieve | ## `pipedrive_file_delete` [Section titled “pipedrive\_file\_delete”](#pipedrive_file_delete) Permanently delete a file by ID. | Name | Type | Required | Description | | --------- | ------ | -------- | ------------------------ | | `file_id` | number | Yes | ID of the file to delete | ## `pipedrive_files_list` [Section titled “pipedrive\_files\_list”](#pipedrive_files_list) List all files uploaded to the Pipedrive account, with optional sorting and pagination. | Name | Type | Required | Description | | ----------------------- | ------- | -------- | --------------------------------------------------- | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page | | `sort` | string | No | Sort field and direction (e.g., `update_time DESC`) | | `include_deleted_files` | boolean | No | Include soft-deleted files in the response | ### Pipelines [Section titled “Pipelines”](#pipelines) ## `pipedrive_pipeline_create` [Section titled “pipedrive\_pipeline\_create”](#pipedrive_pipeline_create) Create a new sales pipeline. | Name | Type | Required | Description | | ------------------ | ------- | -------- | -------------------------------------------- | | `name` | string | Yes | Pipeline name | | `deal_probability` | boolean | No | Whether deal probability tracking is enabled | | `order_nr` | number | No | Display order of the pipeline | ## `pipedrive_pipeline_get` [Section titled “pipedrive\_pipeline\_get”](#pipedrive_pipeline_get) Retrieve full details of a pipeline by ID, including its stages. | Name | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------ | | `pipeline_id` | number | Yes | ID of the pipeline to retrieve | ## `pipedrive_pipeline_update` [Section titled “pipedrive\_pipeline\_update”](#pipedrive_pipeline_update) Update properties of an existing pipeline. | Name | Type | Required | Description | | ------------------ | ------- | -------- | ----------------------------------------------------- | | `pipeline_id` | number | Yes | ID of the pipeline to update | | `name` | string | No | New pipeline name | | `deal_probability` | boolean | No | Enable or disable probability tracking | | `order_nr` | number | No | New display order | | `active` | boolean | No | Archive (`false`) or reactivate (`true`) the pipeline | ## `pipedrive_pipeline_delete` [Section titled “pipedrive\_pipeline\_delete”](#pipedrive_pipeline_delete) Permanently delete a pipeline and all its stages. Deals in the pipeline are not deleted but lose their pipeline association. | Name | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------- | | `pipeline_id` | number | Yes | ID of the pipeline to delete | ## `pipedrive_pipelines_list` [Section titled “pipedrive\_pipelines\_list”](#pipedrive_pipelines_list) List all pipelines in the account. This tool takes no required parameters. ### Stages [Section titled “Stages”](#stages) ## `pipedrive_stage_create` [Section titled “pipedrive\_stage\_create”](#pipedrive_stage_create) Create a new stage in a pipeline. | Name | Type | Required | Description | | ------------------ | ------- | -------- | ------------------------------------------------------------- | | `name` | string | Yes | Stage name | | `pipeline_id` | number | Yes | ID of the pipeline this stage belongs to | | `deal_probability` | number | No | Default win probability for deals entering this stage (0–100) | | `rotten_flag` | boolean | No | Enable rotting for deals that stay in this stage too long | | `rotten_days` | number | No | Number of days before a deal is marked as rotten | | `order_nr` | number | No | Display order within the pipeline | ## `pipedrive_stage_get` [Section titled “pipedrive\_stage\_get”](#pipedrive_stage_get) Retrieve details of a stage by ID. | Name | Type | Required | Description | | ---------- | ------ | -------- | --------------------------- | | `stage_id` | number | Yes | ID of the stage to retrieve | ## `pipedrive_stage_update` [Section titled “pipedrive\_stage\_update”](#pipedrive_stage_update) Update properties of an existing stage. | Name | Type | Required | Description | | ------------------ | ------- | -------- | -------------------------------------------------- | | `stage_id` | number | Yes | ID of the stage to update | | `name` | string | No | New stage name | | `pipeline_id` | number | No | Move the stage to a different pipeline | | `deal_probability` | number | No | New default win probability | | `rotten_flag` | boolean | No | Enable or disable rotting | | `rotten_days` | number | No | New rotten days threshold | | `order_nr` | number | No | New display order | | `active_flag` | boolean | No | Archive (`false`) or reactivate (`true`) the stage | ## `pipedrive_stages_list` [Section titled “pipedrive\_stages\_list”](#pipedrive_stages_list) List all stages, optionally filtered by pipeline. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------- | | `pipeline_id` | number | No | Filter stages to this pipeline ID | ### Products [Section titled “Products”](#products) ## `pipedrive_product_create` [Section titled “pipedrive\_product\_create”](#pipedrive_product_create) Create a new product in the Pipedrive product catalog. | Name | Type | Required | Description | | ------------- | ------ | -------- | ----------------------------------------------------- | | `name` | string | Yes | Product name | | `code` | string | No | Product SKU or code | | `unit` | string | No | Unit of measurement (e.g., `license`, `seat`, `hour`) | | `price` | number | No | Default unit price | | `tax` | number | No | Tax percentage | | `category` | string | No | Product category | | `owner_id` | number | No | User ID of the product owner | | `visible_to` | number | No | Visibility: `1` = owner only, `3` = all users | | `description` | string | No | Product description | ## `pipedrive_product_get` [Section titled “pipedrive\_product\_get”](#pipedrive_product_get) Retrieve full details of a product by ID. | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------- | | `product_id` | number | Yes | ID of the product to retrieve | ## `pipedrive_product_update` [Section titled “pipedrive\_product\_update”](#pipedrive_product_update) Update properties of an existing product. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------- | | `product_id` | number | Yes | ID of the product to update | | `name` | string | No | New product name | | `code` | string | No | New product code | | `unit` | string | No | New unit of measurement | | `price` | number | No | New unit price | | `tax` | number | No | New tax percentage | | `category` | string | No | New product category | | `description` | string | No | New product description | | `visible_to` | number | No | New visibility setting | ## `pipedrive_product_delete` [Section titled “pipedrive\_product\_delete”](#pipedrive_product_delete) Permanently delete a product by ID. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------- | | `product_id` | number | Yes | ID of the product to delete | ## `pipedrive_products_list` [Section titled “pipedrive\_products\_list”](#pipedrive_products_list) List all products in the catalog with optional filtering and pagination. | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------------------------------- | | `filter_id` | number | No | ID of a saved filter to apply | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page. Max 500, defaults to 100 | | `sort` | string | No | Sort field and direction | ### Goals [Section titled “Goals”](#goals) ## `pipedrive_goal_create` [Section titled “pipedrive\_goal\_create”](#pipedrive_goal_create) Create a new sales goal for a user or team. | Name | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------------------------------------------------------------- | | `title` | string | Yes | Goal title | | `assignee` | object | Yes | Goal assignee: `{ "id": 1, "type": "person" }` or `{ "id": 2, "type": "company" }` | | `type` | object | Yes | Goal type: `{ "name": "deals_won", "params": { "pipeline_id": [1] } }` | | `expected_outcome` | object | No | `{ "target": 50000, "tracking_metric": "sum", "currency_id": 1 }` | | `duration` | object | No | `{ "start": "2026-01-01", "end": "2026-12-31" }` | | `interval` | string | Yes | Reporting interval: `weekly`, `monthly`, `quarterly`, `yearly` | ## `pipedrive_goal_get` [Section titled “pipedrive\_goal\_get”](#pipedrive_goal_get) Retrieve full details and current progress of a goal by ID. | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------- | | `goal_id` | string | Yes | UUID of the goal to retrieve | ## `pipedrive_goal_update` [Section titled “pipedrive\_goal\_update”](#pipedrive_goal_update) Update an existing goal. | Name | Type | Required | Description | | ------------------ | ------ | -------- | --------------------------- | | `goal_id` | string | Yes | UUID of the goal to update | | `title` | string | No | New goal title | | `assignee` | object | No | New assignee object | | `type` | object | No | New goal type object | | `expected_outcome` | object | No | New expected outcome object | | `duration` | object | No | New duration object | | `interval` | string | No | New reporting interval | ## `pipedrive_goals_list` [Section titled “pipedrive\_goals\_list”](#pipedrive_goals_list) List all goals with optional filtering by type, assignee, or active status. | Name | Type | Required | Description | | --------------- | ------- | -------- | ------------------------------------------------------- | | `type_name` | string | No | Filter by goal type name (e.g., `deals_won`, `revenue`) | | `assignee_id` | number | No | Filter by assignee user ID | | `assignee_type` | string | No | Filter by assignee type: `person` or `company` | | `is_active` | boolean | No | Filter by active status | | `start` | number | No | Pagination offset | | `limit` | number | No | Number of results per page | ### Users [Section titled “Users”](#users) ## `pipedrive_user_get` [Section titled “pipedrive\_user\_get”](#pipedrive_user_get) Retrieve details of a Pipedrive user by ID, including their role, email, and active status. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------- | | `user_id` | number | Yes | ID of the user to retrieve | ## `pipedrive_users_list` [Section titled “pipedrive\_users\_list”](#pipedrive_users_list) List all users in the Pipedrive account. This tool takes no required parameters. ### Webhooks [Section titled “Webhooks”](#webhooks) ## `pipedrive_webhook_create` [Section titled “pipedrive\_webhook\_create”](#pipedrive_webhook_create) Register a webhook to receive real-time HTTP notifications when Pipedrive objects are created, updated, or deleted. | Name | Type | Required | Description | | ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `subscription_url` | string | Yes | HTTPS endpoint that will receive webhook payloads | | `event_action` | string | Yes | Action to subscribe to: `*` (all), `added`, `updated`, `deleted`, `merged` | | `event_object` | string | Yes | Object type to subscribe to: `*` (all), `deal`, `person`, `organization`, `lead`, `activity`, `note`, `pipeline`, `stage`, `product` | ## `pipedrive_webhook_delete` [Section titled “pipedrive\_webhook\_delete”](#pipedrive_webhook_delete) Delete a webhook subscription by ID. The endpoint will stop receiving notifications immediately. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------- | | `webhook_id` | number | Yes | ID of the webhook to delete | ## `pipedrive_webhooks_list` [Section titled “pipedrive\_webhooks\_list”](#pipedrive_webhooks_list) List all active webhook subscriptions for the account. This tool takes no required parameters. ### Filters [Section titled “Filters”](#filters) ## `pipedrive_filter_create` [Section titled “pipedrive\_filter\_create”](#pipedrive_filter_create) Create a reusable saved filter that can be passed as `filter_id` to any list endpoint. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------------------------------------------------- | | `name` | string | Yes | Filter name | | `conditions` | object | Yes | Filter conditions — see structure below | | `type` | string | Yes | Object type this filter applies to: `deals`, `leads`, `org`, `people`, `products`, `activity` | **`conditions` structure:** The `conditions` object uses a nested `glue`/`conditions` format. `glue` is either `"and"` or `"or"` and controls how conditions in the group are combined. ```json 1 { 2 "glue": "and", 3 "conditions": [ 4 { 5 "glue": "and", 6 "conditions": [ 7 { 8 "object": "deal", 9 "field_id": "value", 10 "operator": ">", 11 "value": "10000", 12 "extra_value": null 13 }, 14 { 15 "object": "deal", 16 "field_id": "status", 17 "operator": "=", 18 "value": "open", 19 "extra_value": null 20 } 21 ] 22 } 23 ] 24 } ``` Each condition has: * `object` — the entity type (`deal`, `person`, `org`, `lead`, `activity`, `product`) * `field_id` — the field name (e.g., `value`, `status`, `stage_id`, `owner_id`) * `operator` — comparison operator: `=`, `!=`, `<`, `>`, `<=`, `>=`, `CONTAINS`, `NOT CONTAINS`, `IS NULL`, `IS NOT NULL` * `value` — the comparison value (always a string, even for numbers) * `extra_value` — used for range operators; `null` otherwise ## `pipedrive_filter_get` [Section titled “pipedrive\_filter\_get”](#pipedrive_filter_get) Retrieve a saved filter and its conditions by ID. | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------- | | `filter_id` | number | Yes | ID of the filter to retrieve | ## `pipedrive_filters_list` [Section titled “pipedrive\_filters\_list”](#pipedrive_filters_list) List all saved filters, optionally scoped to a specific object type. | Name | Type | Required | Description | | ------ | ------ | -------- | -------------------------------------------------------------------------------- | | `type` | string | No | Filter by object type: `deals`, `leads`, `org`, `people`, `products`, `activity` | --- # DOCUMENT BOUNDARY --- # Salesforce > Connect to Salesforce CRM. Manage leads, opportunities, accounts, and customer relationships Connect to Salesforce CRM. Manage leads, opportunities, accounts, and customer relationships ![Salesforce logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/sales_force.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Salesforce connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. You’ll need your app credentials from the [Salesforce Developer Console](https://developer.salesforce.com/). 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. * Find **Salesforce** from the list of providers and click **Create**. Note By default, a connection using Scalekit’s credentials will be created. If you are testing, go directly to the next section. Before going to production, update your connection by following the steps below. * Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.BWy0VRMr.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Log in to [Salesforce](https://login.salesforce.com) and go to **Setup**. * In the Quick Find box, search for **App Manager** and click to open it. * Click **New Connected App**. * Enter a name for your app, check the **Enable OAuth Settings** checkbox, and paste the redirect URI in the **Callback URL** field. ![New Connected App form in Salesforce](/.netlify/images?url=_astro%2Fadd-redirect-uri.DBGMsY-5.png\&w=1440\&h=1000\&dpl=69cce21a4f77360008b1503a) * Select the required OAuth scopes for your application. 2. ### Get client credentials * In your Connected App settings, note the following: * **Consumer Key** — listed under **OAuth Settings** * **Consumer Secret** — click **Reveal** to view and copy 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (Consumer Key from above) * Client Secret (Consumer Secret from above) * Permissions (scopes — see [Salesforce OAuth Scopes documentation](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_scopes.htm\&type=5)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.HJl-c2GR.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Salesforce account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. **Don’t worry about the Salesforce domain or API version in the path.** Scalekit automatically resolves `{{domain}}` and `{{version}}` from the connected account’s configuration. For example, a request with `path="/chatter/users/me"` will be sent to `https://mycompany.my.salesforce.com/services/data/v58.0/chatter/users/me` automatically. You can interact with Salesforce in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'salesforce'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Salesforce:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/chatter/users/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "salesforce" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Salesforce:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/chatter/users/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `salesforce_account_create` [Section titled “salesforce\_account\_create”](#salesforce_account_create) Create a new Account in Salesforce. Supports standard fields | Name | Type | Required | Description | | ------------------- | ------- | -------- | ----------------------------------- | | `AccountNumber` | string | No | Account number for the organization | | `AnnualRevenue` | number | No | Annual revenue | | `BillingCity` | string | No | Billing city | | `BillingCountry` | string | No | Billing country | | `BillingPostalCode` | string | No | Billing postal code | | `BillingState` | string | No | Billing state/province | | `BillingStreet` | string | No | Billing street | | `Description` | string | No | Description | | `Industry` | string | No | Industry | | `Name` | string | Yes | Account Name | | `NumberOfEmployees` | integer | No | Number of employees | | `OwnerId` | string | No | Record owner (User/Queue Id) | | `Phone` | string | No | Main phone number | | `RecordTypeId` | string | No | Record Type Id | | `Website` | string | No | Website URL | ## `salesforce_account_delete` [Section titled “salesforce\_account\_delete”](#salesforce_account_delete) Delete an existing Account from Salesforce by account ID. This is a destructive operation that permanently removes the account record. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------- | | `account_id` | string | Yes | ID of the account to delete | ## `salesforce_account_get` [Section titled “salesforce\_account\_get”](#salesforce_account_get) Retrieve details of a specific account from Salesforce by account ID. Returns account properties and associated data. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------------- | | `account_id` | string | Yes | ID of the account to retrieve | | `fields` | string | No | Comma-separated list of fields to include in the response | ## `salesforce_account_update` [Section titled “salesforce\_account\_update”](#salesforce_account_update) Update an existing Account in Salesforce by account ID. Allows updating account properties like name, phone, website, industry, billing information, and more. | Name | Type | Required | Description | | ------------------------- | ------- | -------- | ----------------------------------- | | `AccountNumber` | string | No | Account number for the organization | | `AccountSource` | string | No | Lead source for this account | | `AnnualRevenue` | number | No | Annual revenue | | `BillingCity` | string | No | Billing city | | `BillingCountry` | string | No | Billing country | | `BillingGeocodeAccuracy` | string | No | Billing geocode accuracy | | `BillingLatitude` | number | No | Billing address latitude | | `BillingLongitude` | number | No | Billing address longitude | | `BillingPostalCode` | string | No | Billing postal code | | `BillingState` | string | No | Billing state/province | | `BillingStreet` | string | No | Billing street | | `CleanStatus` | string | No | Data.com clean status | | `Description` | string | No | Description | | `DunsNumber` | string | No | D-U-N-S Number | | `Fax` | string | No | Fax number | | `Industry` | string | No | Industry | | `Jigsaw` | string | No | Data.com key | | `JigsawCompanyId` | string | No | Jigsaw company ID | | `NaicsCode` | string | No | NAICS code | | `NaicsDesc` | string | No | NAICS description | | `Name` | string | No | Account Name | | `NumberOfEmployees` | integer | No | Number of employees | | `OwnerId` | string | No | Record owner (User/Queue Id) | | `Ownership` | string | No | Ownership type | | `ParentId` | string | No | Parent Account Id | | `Phone` | string | No | Main phone number | | `Rating` | string | No | Account rating | | `RecordTypeId` | string | No | Record Type Id | | `ShippingCity` | string | No | Shipping city | | `ShippingCountry` | string | No | Shipping country | | `ShippingGeocodeAccuracy` | string | No | Shipping geocode accuracy | | `ShippingLatitude` | number | No | Shipping address latitude | | `ShippingLongitude` | number | No | Shipping address longitude | | `ShippingPostalCode` | string | No | Shipping postal code | | `ShippingState` | string | No | Shipping state/province | | `ShippingStreet` | string | No | Shipping street | | `Sic` | string | No | SIC code | | `SicDesc` | string | No | SIC description | | `Site` | string | No | Account site or location | | `TickerSymbol` | string | No | Stock ticker symbol | | `Tradestyle` | string | No | Trade style name | | `Type` | string | No | Account type | | `Website` | string | No | Website URL | | `YearStarted` | string | No | Year the company started | | `account_id` | string | Yes | ID of the account to update | ## `salesforce_accounts_list` [Section titled “salesforce\_accounts\_list”](#salesforce_accounts_list) Retrieve a list of accounts from Salesforce using a pre-built SOQL query. Returns basic account information. | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------------------ | | `limit` | number | Yes | Number of results to return per page | ## `salesforce_chatter_comment_create` [Section titled “salesforce\_chatter\_comment\_create”](#salesforce_chatter_comment_create) Add a comment to a Salesforce Chatter post (feed element). | Name | Type | Required | Description | | ----------------- | ------ | -------- | ---------------------------------------- | | `feed_element_id` | string | Yes | The ID of the Chatter post to comment on | | `text` | string | Yes | The text body of the comment | ## `salesforce_chatter_comment_delete` [Section titled “salesforce\_chatter\_comment\_delete”](#salesforce_chatter_comment_delete) Delete a comment from a Salesforce Chatter post. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------- | | `comment_id` | string | Yes | The ID of the Chatter comment to delete | ## `salesforce_chatter_comments_list` [Section titled “salesforce\_chatter\_comments\_list”](#salesforce_chatter_comments_list) List all comments on a Salesforce Chatter post (feed element). | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------------------------------- | | `feed_element_id` | string | Yes | The ID of the Chatter post to list comments for | | `page` | string | No | Page token for retrieving the next page of results | | `page_size` | number | No | Number of comments to return per page (default: 25, max: 100) | ## `salesforce_chatter_post_create` [Section titled “salesforce\_chatter\_post\_create”](#salesforce_chatter_post_create) Create a new post (feed element) on a Salesforce Chatter feed. Use ‘me’ as subject\_id to post to the current user’s feed. | Name | Type | Required | Description | | ------------------ | ------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | `text` | string | Yes | The text body of the Chatter post | | `is_rich_text` | boolean | No | If true, the text body will be treated as HTML rich text | | `message_segments` | array | No | Advanced: raw Salesforce message segments array for full rich text control (bold, italic, links, mentions, etc.) | | `subject_id` | string | No | The ID of the subject (user, record, or group) to post to. Use ‘me’ for the current user’s feed | ## `salesforce_chatter_post_delete` [Section titled “salesforce\_chatter\_post\_delete”](#salesforce_chatter_post_delete) Delete a Salesforce Chatter post (feed element) by its ID. | Name | Type | Required | Description | | ----------------- | ------ | -------- | ------------------------------------ | | `feed_element_id` | string | Yes | The ID of the Chatter post to delete | ## `salesforce_chatter_post_get` [Section titled “salesforce\_chatter\_post\_get”](#salesforce_chatter_post_get) Retrieve a specific Salesforce Chatter post (feed element) by its ID. | Name | Type | Required | Description | | ----------------- | ------ | -------- | ----------------------------------------------------- | | `feed_element_id` | string | Yes | The ID of the Chatter feed element (post) to retrieve | ## `salesforce_chatter_posts_search` [Section titled “salesforce\_chatter\_posts\_search”](#salesforce_chatter_posts_search) Search Salesforce Chatter posts (feed elements) by keyword across all feeds. | Name | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------ | | `q` | string | Yes | Search query string to find matching Chatter posts | | `page` | string | No | Page token for retrieving the next page of results | | `page_size` | number | No | Number of results to return per page (default: 25, max: 100) | ## `salesforce_chatter_user_feed_list` [Section titled “salesforce\_chatter\_user\_feed\_list”](#salesforce_chatter_user_feed_list) Retrieve feed elements (posts) from a Salesforce user’s Chatter news feed. Use ‘me’ as the user ID to get the current user’s feed. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------------------------------------------------------------- | | `user_id` | string | Yes | The ID of the user whose Chatter feed to retrieve. Use ‘me’ for the current user | | `page` | string | No | Page token for retrieving the next page of results | | `page_size` | number | No | Number of feed elements to return per page (default: 25, max: 100) | | `sort_param` | string | No | Sort order for feed elements. Options: LastModifiedDateDesc (default), CreatedDateDesc, MostRecentActivity | ## `salesforce_composite` [Section titled “salesforce\_composite”](#salesforce_composite) Execute multiple Salesforce REST API requests in a single call using the Composite API. Allows for efficient batch operations and related data retrieval. | Name | Type | Required | Description | | ------------------- | ------ | -------- | ------------------------------------------------------------------- | | `composite_request` | string | Yes | JSON string containing composite request with multiple sub-requests | ## `salesforce_contact_create` [Section titled “salesforce\_contact\_create”](#salesforce_contact_create) Create a new contact in Salesforce. Allows setting contact properties like name, email, phone, account association, and other standard fields. | Name | Type | Required | Description | | ------------------- | ------ | -------- | -------------------------------------------------- | | `AccountId` | string | No | Salesforce Account Id associated with this contact | | `Department` | string | No | Department of the contact | | `Description` | string | No | Free-form description | | `Email` | string | No | Email address of the contact | | `FirstName` | string | No | First name of the contact | | `LastName` | string | Yes | Last name of the contact (required) | | `LeadSource` | string | No | Lead source for the contact | | `MailingCity` | string | No | Mailing city | | `MailingCountry` | string | No | Mailing country | | `MailingPostalCode` | string | No | Mailing postal code | | `MailingState` | string | No | Mailing state/province | | `MailingStreet` | string | No | Mailing street | | `MobilePhone` | string | No | Mobile phone of the contact | | `Phone` | string | No | Phone number of the contact | | `Title` | string | No | Job title of the contact | ## `salesforce_contact_get` [Section titled “salesforce\_contact\_get”](#salesforce_contact_get) Retrieve details of a specific contact from Salesforce by contact ID. Returns contact properties and associated data. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------------- | | `contact_id` | string | Yes | ID of the contact to retrieve | | `fields` | string | No | Comma-separated list of fields to include in the response | ## `salesforce_dashboard_clone` [Section titled “salesforce\_dashboard\_clone”](#salesforce_dashboard_clone) Clone an existing dashboard in Salesforce. Creates a copy of the source dashboard in the specified folder. | Name | Type | Required | Description | | --------------------- | ------ | -------- | ------------------------------------ | | `folderId` | string | No | Folder to place the cloned dashboard | | `source_dashboard_id` | string | Yes | ID of the dashboard to clone | ## `salesforce_dashboard_get` [Section titled “salesforce\_dashboard\_get”](#salesforce_dashboard_get) Retrieve dashboard data and results from Salesforce by dashboard ID. Returns dashboard component data and results from all underlying reports. | Name | Type | Required | Description | | -------------- | ------ | -------- | -------------------------------------------------------- | | `dashboard_id` | string | Yes | ID of the dashboard to retrieve | | `filter1` | string | No | First dashboard filter value (DashboardFilterOption ID) | | `filter2` | string | No | Second dashboard filter value (DashboardFilterOption ID) | | `filter3` | string | No | Third dashboard filter value (DashboardFilterOption ID) | ## `salesforce_dashboard_metadata_get` [Section titled “salesforce\_dashboard\_metadata\_get”](#salesforce_dashboard_metadata_get) Retrieve metadata for a Salesforce dashboard, including dashboard components, filters, layout, and the running user. | Name | Type | Required | Description | | -------------- | ------ | -------- | ----------------------------------------- | | `dashboard_id` | string | Yes | The unique ID of the Salesforce dashboard | ## `salesforce_dashboard_update` [Section titled “salesforce\_dashboard\_update”](#salesforce_dashboard_update) Save dashboard filters (sticky filters) in Salesforce. Use GET dashboard first to find filter IDs. | Name | Type | Required | Description | | -------------- | ---------------- | -------- | --------------------------------- | | `dashboard_id` | string | Yes | ID of the dashboard to update | | `filters` | `array` | No | Dashboard filters to save (array) | ## `salesforce_global_describe` [Section titled “salesforce\_global\_describe”](#salesforce_global_describe) Retrieve metadata about all available SObjects in the Salesforce organization. Returns list of all objects with basic information. ## `salesforce_limits_get` [Section titled “salesforce\_limits\_get”](#salesforce_limits_get) Retrieve organization limits information from Salesforce. Returns API usage limits, data storage limits, and other organizational constraints. ## `salesforce_object_describe` [Section titled “salesforce\_object\_describe”](#salesforce_object_describe) Retrieve detailed metadata about a specific SObject in Salesforce. Returns fields, relationships, and other object metadata. | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------- | | `sobject` | string | Yes | SObject API name to describe | ## `salesforce_opportunities_list` [Section titled “salesforce\_opportunities\_list”](#salesforce_opportunities_list) Retrieve a list of opportunities from Salesforce using a pre-built SOQL query. Returns basic opportunity information. | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------------------ | | `limit` | number | No | Number of results to return per page | ## `salesforce_opportunity_create` [Section titled “salesforce\_opportunity\_create”](#salesforce_opportunity_create) Create a new opportunity in Salesforce. Allows setting opportunity properties like name, amount, stage, close date, and account association. | Name | Type | Required | Description | | ---------------------- | ------ | -------- | -------------------------------------------------------------------- | | `AccountId` | string | No | Associated Account Id | | `Amount` | number | No | Opportunity amount | | `CampaignId` | string | No | Related Campaign Id | | `CloseDate` | string | Yes | Expected close date (YYYY-MM-DD, required) | | `Custom_Field__c` | string | No | Example custom field (replace with your org’s custom field API name) | | `Description` | string | No | Opportunity description | | `ForecastCategoryName` | string | No | Forecast category name | | `LeadSource` | string | No | Lead source | | `Name` | string | Yes | Opportunity name (required) | | `NextStep` | string | No | Next step in the sales process | | `OwnerId` | string | No | Record owner (User/Queue Id) | | `PricebookId` | string | No | Associated Price Book Id | | `Probability` | number | No | Probability percentage (0–100) | | `RecordTypeId` | string | No | Record Type Id for Opportunity | | `StageName` | string | Yes | Current sales stage (required) | | `Type` | string | No | Opportunity type | ## `salesforce_opportunity_get` [Section titled “salesforce\_opportunity\_get”](#salesforce_opportunity_get) Retrieve details of a specific opportunity from Salesforce by opportunity ID. Returns opportunity properties and associated data. | Name | Type | Required | Description | | ---------------- | ------ | -------- | --------------------------------------------------------- | | `fields` | string | No | Comma-separated list of fields to include in the response | | `opportunity_id` | string | Yes | ID of the opportunity to retrieve | ## `salesforce_opportunity_update` [Section titled “salesforce\_opportunity\_update”](#salesforce_opportunity_update) Update an existing opportunity in Salesforce by opportunity ID. Allows updating opportunity properties like name, amount, stage, and close date. | Name | Type | Required | Description | | ---------------------- | ------ | -------- | -------------------------------- | | `AccountId` | string | No | Associated Account Id | | `Amount` | number | No | Opportunity amount | | `CampaignId` | string | No | Related Campaign Id | | `CloseDate` | string | No | Expected close date (YYYY-MM-DD) | | `Description` | string | No | Opportunity description | | `ForecastCategoryName` | string | No | Forecast category name | | `LeadSource` | string | No | Lead source | | `Name` | string | No | Opportunity name | | `NextStep` | string | No | Next step in the sales process | | `OwnerId` | string | No | Record owner (User/Queue Id) | | `Pricebook2Id` | string | No | Associated Price Book Id | | `Probability` | number | No | Probability percentage (0–100) | | `RecordTypeId` | string | No | Record Type Id for Opportunity | | `StageName` | string | No | Current sales stage | | `Type` | string | No | Opportunity type | | `opportunity_id` | string | Yes | ID of the opportunity to update | ## `salesforce_query_soql` [Section titled “salesforce\_query\_soql”](#salesforce_query_soql) Execute SOQL queries against Salesforce data. Supports complex queries with joins, filters, and aggregations. | Name | Type | Required | Description | | ------- | ------ | -------- | ---------------------------- | | `query` | string | Yes | SOQL query string to execute | ## `salesforce_query_next_page` [Section titled “salesforce\_query\_next\_page”](#salesforce_query_next_page) Fetch the next page of results from a previous SOQL query. Use the nextRecordsUrl returned when a query response has `done=false`. | Name | Type | Required | Description | | -------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cursor` | string | Yes | The record cursor from a previous SOQL query response. Extract the cursor ID from the nextRecordsUrl (e.g. `01gxx0000002GJm-2000` from `/services/data/v66.0/query/01gxx0000002GJm-2000`) | ## `salesforce_report_create` [Section titled “salesforce\_report\_create”](#salesforce_report_create) Create a new report in Salesforce using the Analytics API. Minimal verified version with only confirmed working fields. | Name | Type | Required | Description | | --------------------- | ------ | -------- | --------------------------------------------------------------------- | | `aggregates` | string | No | Aggregates configuration (JSON array) | | `chart` | string | No | Chart configuration (JSON object) | | `description` | string | No | Report description | | `detailColumns` | string | No | Detail columns (JSON array of field names) | | `folderId` | string | No | Folder ID where report will be stored | | `groupingsAcross` | string | No | Column groupings (JSON array) | | `groupingsDown` | string | No | Row groupings (JSON array) | | `name` | string | Yes | Report name | | `reportBooleanFilter` | string | No | Filter logic | | `reportFilters` | string | No | Report filters (JSON array) | | `reportType` | string | Yes | Report type - object name (e.g., Account, Opportunity, Contact, Lead) | ## `salesforce_report_delete` [Section titled “salesforce\_report\_delete”](#salesforce_report_delete) Delete an existing report from Salesforce by report ID. This is a destructive operation that permanently removes the report and cannot be undone. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------- | | `report_id` | string | Yes | ID of the report to delete | ## `salesforce_report_get` [Section titled “salesforce\_report\_get”](#salesforce_report_get) Retrieve report definition and metadata from Salesforce by report ID. Returns the full report configuration including columns, filters, groupings, and chart settings. | Name | Type | Required | Description | | ----------- | ------ | -------- | ---------------------------- | | `report_id` | string | Yes | ID of the report to retrieve | ## `salesforce_report_metadata_get` [Section titled “salesforce\_report\_metadata\_get”](#salesforce_report_metadata_get) Retrieve report, report type, and related metadata for a Salesforce report. Returns information about report structure, fields, groupings, and configuration. | Name | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------- | | `report_id` | string | Yes | The unique ID of the Salesforce report | ## `salesforce_report_update` [Section titled “salesforce\_report\_update”](#salesforce_report_update) Update an existing report in Salesforce by report ID. Minimal verified version with only confirmed working fields. Only updates fields that are provided. | Name | Type | Required | Description | | --------------------- | ------ | -------- | ------------------------------------------ | | `aggregates` | string | No | Aggregates configuration (JSON array) | | `chart` | string | No | Chart configuration (JSON object) | | `description` | string | No | Updated report description | | `detailColumns` | string | No | Detail columns (JSON array of field names) | | `folderId` | string | No | Move report to different folder | | `groupingsAcross` | string | No | Column groupings (JSON array) | | `groupingsDown` | string | No | Row groupings (JSON array) | | `name` | string | No | Updated report name | | `reportBooleanFilter` | string | No | Filter logic | | `reportFilters` | string | No | Report filters (JSON array) | | `report_id` | string | Yes | ID of the report to update | ## `salesforce_search_parameterized` [Section titled “salesforce\_search\_parameterized”](#salesforce_search_parameterized) Execute parameterized searches against Salesforce data. Provides simplified search interface with predefined parameters. | Name | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------------------- | | `fields` | string | No | Comma-separated list of fields to return | | `search_text` | string | Yes | Text to search for | | `sobject` | string | Yes | SObject type to search in | ## `salesforce_search_sosl` [Section titled “salesforce\_search\_sosl”](#salesforce_search_sosl) Execute SOSL searches against Salesforce data. Performs full-text search across multiple objects and fields. | Name | Type | Required | Description | | -------------- | ------ | -------- | ----------------------------------- | | `search_query` | string | Yes | SOSL search query string to execute | ## `salesforce_sobject_create` [Section titled “salesforce\_sobject\_create”](#salesforce_sobject_create) Create a new record for any Salesforce SObject type (Account, Contact, Lead, Opportunity, custom objects, etc.). Provide the object type and fields as a dynamic object. | Name | Type | Required | Description | | -------------- | -------- | -------- | --------------------------------------------------------------------------------- | | `fields` | `object` | Yes | Object containing field names and values to set on the new record | | `sobject_type` | string | Yes | The Salesforce SObject API name (e.g., Account, Contact, Lead, CustomObject\_\_c) | ## `salesforce_sobject_delete` [Section titled “salesforce\_sobject\_delete”](#salesforce_sobject_delete) Delete a record from any Salesforce SObject type by ID. This is a destructive operation that permanently removes the record. | Name | Type | Required | Description | | -------------- | ------ | -------- | --------------------------------------------------------------------------------- | | `record_id` | string | Yes | ID of the record to delete | | `sobject_type` | string | Yes | The Salesforce SObject API name (e.g., Account, Contact, Lead, CustomObject\_\_c) | ## `salesforce_sobject_get` [Section titled “salesforce\_sobject\_get”](#salesforce_sobject_get) Retrieve a record from any Salesforce SObject type by ID. Optionally specify which fields to return. | Name | Type | Required | Description | | -------------- | ------ | -------- | --------------------------------------------------------------------------------- | | `fields` | string | No | Comma-separated list of fields to include in the response | | `record_id` | string | Yes | ID of the record to retrieve | | `sobject_type` | string | Yes | The Salesforce SObject API name (e.g., Account, Contact, Lead, CustomObject\_\_c) | ## `salesforce_sobject_update` [Section titled “salesforce\_sobject\_update”](#salesforce_sobject_update) Update an existing record for any Salesforce SObject type by ID. Only the fields provided will be updated. | Name | Type | Required | Description | | -------------- | -------- | -------- | --------------------------------------------------------------------------------- | | `fields` | `object` | Yes | Object containing field names and values to update on the record | | `record_id` | string | Yes | ID of the record to update | | `sobject_type` | string | Yes | The Salesforce SObject API name (e.g., Account, Contact, Lead, CustomObject\_\_c) | ## `salesforce_soql_execute` [Section titled “salesforce\_soql\_execute”](#salesforce_soql_execute) Execute custom SOQL queries against Salesforce data. Supports complex queries with joins, filters, aggregations, and custom field selection. | Name | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------- | | `soql_query` | string | Yes | SOQL query string to execute | ## `salesforce_tooling_query_execute` [Section titled “salesforce\_tooling\_query\_execute”](#salesforce_tooling_query_execute) Execute SOQL queries against Salesforce Tooling API to access metadata objects like ApexClass, ApexTrigger, CustomObject, and development metadata. Use this for querying metadata rather than data objects. | Name | Type | Required | Description | | ------------ | ------ | -------- | ------------------------------------------------ | | `soql_query` | string | Yes | SOQL query string to execute against Tooling API | ## `salesforce_tooling_sobject_create` [Section titled “salesforce\_tooling\_sobject\_create”](#salesforce_tooling_sobject_create) Create a new metadata record for any Salesforce Tooling API object type (ApexClass, ApexTrigger, CustomField, etc.). Supports both simple and nested field structures. For CustomField, use FullName and Metadata properties. | Name | Type | Required | Description | | -------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `fields` | `object` | Yes | Object containing field names and values to set on the new metadata record. Supports nested structures for complex metadata types. | | `sobject_type` | string | Yes | The Tooling API object name (e.g., ApexClass, ApexTrigger, CustomObject) | ## `salesforce_tooling_sobject_delete` [Section titled “salesforce\_tooling\_sobject\_delete”](#salesforce_tooling_sobject_delete) Delete a metadata record from any Salesforce Tooling API object type by ID. This is a destructive operation that permanently removes the metadata. | Name | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------------------------------------------ | | `record_id` | string | Yes | ID of the metadata record to delete | | `sobject_type` | string | Yes | The Tooling API object name (e.g., ApexClass, ApexTrigger, CustomObject) | ## `salesforce_tooling_sobject_describe` [Section titled “salesforce\_tooling\_sobject\_describe”](#salesforce_tooling_sobject_describe) Retrieve detailed metadata schema for a specific Tooling API object type. Returns fields, relationships, and other metadata properties. | Name | Type | Required | Description | | --------- | ------ | -------- | ----------------------------------- | | `sobject` | string | Yes | Tooling API object name to describe | ## `salesforce_tooling_sobject_get` [Section titled “salesforce\_tooling\_sobject\_get”](#salesforce_tooling_sobject_get) Retrieve a metadata record from any Salesforce Tooling API object type by ID. Optionally specify which fields to return. | Name | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------------------------------------------ | | `fields` | string | No | Comma-separated list of fields to include in the response | | `record_id` | string | Yes | ID of the metadata record to retrieve | | `sobject_type` | string | Yes | The Tooling API object name (e.g., ApexClass, ApexTrigger, CustomObject) | ## `salesforce_tooling_sobject_update` [Section titled “salesforce\_tooling\_sobject\_update”](#salesforce_tooling_sobject_update) Update an existing metadata record for any Salesforce Tooling API object type by ID. Supports both simple and nested field structures. Only the fields provided will be updated. | Name | Type | Required | Description | | -------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | `fields` | `object` | Yes | Object containing field names and values to update on the metadata record. Supports nested structures for complex metadata types. | | `record_id` | string | Yes | ID of the metadata record to update | | `sobject_type` | string | Yes | The Tooling API object name (e.g., ApexClass, ApexTrigger, CustomObject) | --- # DOCUMENT BOUNDARY --- # ServiceNow > Connect to ServiceNow. Manage incidents, service requests, CMDB, and IT service management workflows Connect to ServiceNow. Manage incidents, service requests, CMDB, and IT service management workflows ![ServiceNow logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/servicenow.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the ServiceNow connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **ServiceNow** and click **Create**. Note By default, a connection using Scalekit’s credentials will be created. If you are testing, go directly to the Usage section. Before going to production, update your connection by following the steps below. * Click **Use your own credentials** and copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.CdC2EtCH.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * In the [ServiceNow Developer Portal](https://developer.servicenow.com/), go to your instance and click **Manage instance password** to find your instance URL. ![ServiceNow Developer Portal manage instance screen](/.netlify/images?url=_astro%2Fmanage-instance.n-OWww19.png\&w=840\&h=799\&dpl=69cce21a4f77360008b1503a) * Log into your ServiceNow instance, navigate to **System OAuth** → **Application Registry**, and click **New** → **Create an OAuth API endpoint for external clients**. * Fill in an app name and paste the copied URI into the **Redirect URL** field, then click **Submit**. 2. ### Get client credentials After submitting, open the newly created record in **System OAuth** → **Application Registry**: * **Client ID** — auto-generated, listed under **Client ID** * **Client Secret** — click the lock icon next to **Client Secret** to reveal it 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from your ServiceNow Application Registry) * Client Secret (from your ServiceNow Application Registry) ![Add credentials for ServiceNow in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s ServiceNow account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. **Don’t worry about your ServiceNow instance domain in the path.** Scalekit automatically resolves `{{domain}}` from the connected account’s configuration. For example, a request with `path="/api/now/table/sys_user"` will be sent to `https://mycompany.service-now.com/api/now/table/sys_user` automatically. You can interact with ServiceNow in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'servicenow'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize ServiceNow:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/api/now/table/sys_user', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "servicenow" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize ServiceNow:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/api/now/table/sys_user", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # SharePoint > Connect to SharePoint. Manage sites, documents, lists, and collaborative content Connect to SharePoint. Manage sites, documents, lists, and collaborative content ![SharePoint logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/sharepoint.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the SharePoint connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **SharePoint** and click **Create**. Copy the redirect URI. It will look like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.DPOy-EMa.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Sign into and go to **Microsoft Entra ID** → **App registrations** → **New registration**. * Enter a name for your app. * Under **Supported account types**, select **Accounts in any organizational directory (Any Azure AD directory - Multitenant)**. * Under **Redirect URI**, select **Web** and paste the redirect URI from step 1. Click **Register**. ![Register an application in Azure portal](/.netlify/images?url=_astro%2Fadd-redirect-uri.B-4Hoff_.png\&w=1440\&h=1020\&dpl=69cce21a4f77360008b1503a) 2. ### Get your client credentials * Go to **Certificates & secrets** → **New client secret**, set an expiry, and click **Add**. Copy the **Value** immediately. * From the **Overview** page, copy the **Application (client) ID**. 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (Application (client) ID from Azure) * Client Secret (from Certificates & secrets) * Permissions (scopes — see [Microsoft Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-reference)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.HJl-c2GR.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s SharePoint account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with SharePoint in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'sharepoint'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize SharePoint:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v1.0/me/sites', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "sharepoint" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize SharePoint:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v1.0/me/sites", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools ## File operations ### Download a file Fetch file metadata via the Scalekit proxy to get a pre-authenticated download URL, then stream the file directly from Microsoft’s CDN. This avoids buffering large files through the proxy and is significantly faster. * Python ```python 1 import requests 2 import scalekit.client, os 3 from dotenv import load_dotenv 4 load_dotenv() 5 6 connection_name = "sharepoint" # get your connection name from connection configurations 7 identifier = "user_123" # your unique user identifier 8 site_id = "" # call GET /v1.0/sites/root to get your site ID 9 10 scalekit_client = 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_ENV_URL"), 14 ) 15 16 filename = "report.pdf" 17 18 # Step 1: Fetch file metadata via Scalekit proxy (authenticated) 19 response = scalekit_client.actions.request( 20 connection_name=connection_name, 21 identifier=identifier, 22 path=f"/v1.0/sites/{site_id}/drive/root:/{filename}", 23 method="GET", 24 query_params={}, 25 ) 26 meta = response.json() 27 28 # Step 2: Stream directly from Microsoft CDN using the pre-authenticated URL 29 # No auth headers needed — the URL is cryptographically signed and expires in ~1 hour 30 download_url = meta["@microsoft.graph.downloadUrl"] 31 32 with requests.get(download_url, stream=True) as r: 33 r.raise_for_status() 34 with open(filename, "wb") as f: 35 for chunk in r.iter_content(chunk_size=8 * 1024 * 1024): # 8 MB chunks 36 f.write(chunk) 37 38 print(f"Downloaded: {filename} ({os.path.getsize(filename):,} bytes)") ``` ### Upload a file Upload a file to SharePoint’s Shared Documents folder. Scalekit injects the OAuth token automatically — your app never handles credentials directly. * Python ```python 1 import mimetypes 2 import scalekit.client, os 3 from dotenv import load_dotenv 4 load_dotenv() 5 6 connection_name = "sharepoint" # get your connection name from connection configurations 7 identifier = "user_123" # your unique user identifier 8 site_id = "" # call GET /v1.0/sites/root to get your site ID 9 10 scalekit_client = 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_ENV_URL"), 14 ) 15 16 filename = "report.pdf" 17 with open(filename, "rb") as f: 18 file_bytes = f.read() 19 20 mime_type = mimetypes.guess_type(filename)[0] or "application/octet-stream" 21 22 response = scalekit_client.actions.request( 23 connection_name=connection_name, 24 identifier=identifier, 25 path=f"/v1.0/sites/{site_id}/drive/root:/{filename}:/content", 26 method="PUT", 27 query_params={}, 28 form_data=file_bytes, 29 headers={"Content-Type": mime_type}, 30 ) 31 32 meta = response.json() 33 print(f"Uploaded: {meta['name']} → {meta['webUrl']}") ``` --- # DOCUMENT BOUNDARY --- # Slack > Connect to Slack workspace. Send Messages as Bots or on behalf of users Connect to Slack workspace. Send Messages as Bots or on behalf of users ![Slack logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/slack.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Slack connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. Then complete the configuration in your application as follows: 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Slack** and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.Cu0ZHq_3.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Log in to [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App**. * Select **From scratch**, enter an app name, and select your workspace. * Go to **OAuth & Permissions** and scroll to **Redirect URLs**. * Click **Add New Redirect URL** and paste the redirect URI from Scalekit. Click **Add**. ![Add redirect URL in Slack](/.netlify/images?url=_astro%2Fadd-redirect-url.CltGMArX.gif\&w=1248\&h=848\&dpl=69cce21a4f77360008b1503a) 2. ### Enable distribution * In your Slack app settings, go to **Manage Distribution** and enable it. ![Enable Slack app distribution](/.netlify/images?url=_astro%2Fenable-distribution.Z36koa3D.png\&w=1352\&h=952\&dpl=69cce21a4f77360008b1503a) * From **Basic Information**, copy the **Client ID** and **Client Secret**. 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID * Client Secret * Permissions (scopes — see [Slack Scopes documentation](https://api.slack.com/scopes)) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.HJl-c2GR.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Slack account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Slack in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'slack'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Slack:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/api/auth.test', 29 method: 'POST', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "slack" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Slack:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/api/auth.test", 30 method="POST" 31 ) 32 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `slack_add_reaction` [Section titled “slack\_add\_reaction”](#slack_add_reaction) Add an emoji reaction to a message in Slack. Requires a valid Slack OAuth2 connection with reactions:write scope. | Name | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------- | | `channel` | string | Yes | Channel ID or channel name where the message exists | | `name` | string | Yes | Emoji name to react with (without colons) | | `timestamp` | string | Yes | Timestamp of the message to add reaction to | ## `slack_create_channel` [Section titled “slack\_create\_channel”](#slack_create_channel) Creates a new public or private channel in a Slack workspace. Requires a valid Slack OAuth2 connection with channels:manage scope for public channels or groups:write scope for private channels. | Name | Type | Required | Description | | ------------ | ------- | -------- | ---------------------------------------------------------- | | `is_private` | boolean | No | Create a private channel instead of public | | `name` | string | Yes | Name of the channel to create (without # prefix) | | `team_id` | string | No | Encoded team ID to create channel in (if using org tokens) | ## `slack_delete_message` [Section titled “slack\_delete\_message”](#slack_delete_message) Deletes a message from a Slack channel or direct message. Requires a valid Slack OAuth2 connection with chat:write scope. | Name | Type | Required | Description | | --------- | ------ | -------- | --------------------------------------------------------------------------------- | | `channel` | string | Yes | Channel ID, channel name (#general), or user ID for DM where the message was sent | | `ts` | string | Yes | Timestamp of the message to delete | ## `slack_fetch_conversation_history` [Section titled “slack\_fetch\_conversation\_history”](#slack_fetch_conversation_history) Fetches conversation history from a Slack channel or direct message with pagination support. Requires a valid Slack OAuth2 connection with channels:history scope. | Name | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------------ | | `channel` | string | Yes | Channel ID, channel name (#general), or user ID for DM | | `cursor` | string | No | Paginate through collections by cursor for pagination | | `latest` | string | No | End of time range of messages to include in results | | `limit` | integer | No | Number of messages to return (1-1000, default 100) | | `oldest` | string | No | Start of time range of messages to include in results | ## `slack_get_conversation_info` [Section titled “slack\_get\_conversation\_info”](#slack_get_conversation_info) Retrieve information about a Slack channel, including metadata, settings, and member count. Requires a valid Slack OAuth2 connection with channels:read scope. | Name | Type | Required | Description | | --------------------- | ------- | -------- | ------------------------------------------------------------ | | `channel` | string | Yes | Channel ID, channel name (#general), or user ID for DM | | `include_locale` | boolean | No | Set to true to include the locale for this conversation | | `include_num_members` | boolean | No | Set to true to include the member count for the conversation | ## `slack_get_conversation_replies` [Section titled “slack\_get\_conversation\_replies”](#slack_get_conversation_replies) Retrieve replies to a specific message thread in a Slack channel or direct message. Requires a valid Slack OAuth2 connection with channels:history or groups:history scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | ----------------------------------------------------------- | | `channel` | string | Yes | Channel ID, channel name (#general), or user ID for DM | | `cursor` | string | No | Pagination cursor for retrieving next page of results | | `inclusive` | boolean | No | Include messages with latest or oldest timestamp in results | | `latest` | string | No | End of time range of messages to include in results | | `limit` | integer | No | Number of messages to return (default 100, max 1000) | | `oldest` | string | No | Start of time range of messages to include in results | | `ts` | string | Yes | Timestamp of the parent message to get replies for | ## `slack_get_user_info` [Section titled “slack\_get\_user\_info”](#slack_get_user_info) Retrieves detailed information about a specific Slack user, including profile data, status, and workspace information. Requires a valid Slack OAuth2 connection with users:read scope. | Name | Type | Required | Description | | ---------------- | ------- | -------- | ------------------------------------------------------ | | `include_locale` | boolean | No | Set to true to include locale information for the user | | `user` | string | Yes | User ID to get information about | ## `slack_get_user_presence` [Section titled “slack\_get\_user\_presence”](#slack_get_user_presence) Gets the current presence status of a Slack user (active, away, etc.). Indicates whether the user is currently online and available. Requires a valid Slack OAuth2 connection with users:read scope. | Name | Type | Required | Description | | ------ | ------ | -------- | ----------------------------- | | `user` | string | Yes | User ID to check presence for | ## `slack_invite_users_to_channel` [Section titled “slack\_invite\_users\_to\_channel”](#slack_invite_users_to_channel) Invites one or more users to a Slack channel. Requires a valid Slack OAuth2 connection with channels:write scope for public channels or groups:write for private channels. | Name | Type | Required | Description | | --------- | ------ | -------- | --------------------------------------------------------- | | `channel` | string | Yes | Channel ID or channel name (#general) to invite users to | | `users` | string | Yes | Comma-separated list of user IDs to invite to the channel | ## `slack_join_conversation` [Section titled “slack\_join\_conversation”](#slack_join_conversation) Joins an existing Slack channel. The authenticated user will become a member of the channel. Requires a valid Slack OAuth2 connection with channels:write scope for public channels. | Name | Type | Required | Description | | --------- | ------ | -------- | --------------------------------------------- | | `channel` | string | Yes | Channel ID or channel name (#general) to join | ## `slack_leave_conversation` [Section titled “slack\_leave\_conversation”](#slack_leave_conversation) Leaves a Slack channel. The authenticated user will be removed from the channel and will no longer receive messages from it. Requires a valid Slack OAuth2 connection with channels:write scope for public channels or groups:write for private channels. | Name | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------------- | | `channel` | string | Yes | Channel ID or channel name (#general) to leave | ## `slack_list_channels` [Section titled “slack\_list\_channels”](#slack_list_channels) List all public and private channels in a Slack workspace that the authenticated user has access to. Requires a valid Slack OAuth2 connection with channels:read, groups:read, mpim:read, and/or im:read scopes depending on conversation types needed. | Name | Type | Required | Description | | ------------------ | ------- | -------- | ------------------------------------------------------------------------- | | `cursor` | string | No | Pagination cursor for retrieving next page of results | | `exclude_archived` | boolean | No | Exclude archived channels from the list | | `limit` | integer | No | Number of channels to return (default 100, max 1000) | | `team_id` | string | No | Encoded team ID to list channels for (optional) | | `types` | string | No | Mix and match channel types (public\_channel, private\_channel, mpim, im) | ## `slack_list_users` [Section titled “slack\_list\_users”](#slack_list_users) Lists all users in a Slack workspace, including information about their status, profile, and presence. Requires a valid Slack OAuth2 connection with users:read scope. | Name | Type | Required | Description | | ---------------- | ------- | -------- | -------------------------------------------------------- | | `cursor` | string | No | Pagination cursor for fetching additional pages of users | | `include_locale` | boolean | No | Set to true to include locale information for each user | | `limit` | number | No | Number of users to return (1-1000) | | `team_id` | string | No | Encoded team ID to list users for (if using org tokens) | ## `slack_lookup_user_by_email` [Section titled “slack\_lookup\_user\_by\_email”](#slack_lookup_user_by_email) Find a user by their registered email address in a Slack workspace. Requires a valid Slack OAuth2 connection with users:read.email scope. Cannot be used by custom bot users. | Name | Type | Required | Description | | ------- | ------ | -------- | ------------------------------------ | | `email` | string | Yes | Email address to search for users by | ## `slack_pin_message` [Section titled “slack\_pin\_message”](#slack_pin_message) Pin a message to a Slack channel. Pinned messages are highlighted and easily accessible to channel members. Requires a valid Slack OAuth2 connection with pins:write scope. | Name | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------- | | `channel` | string | Yes | Channel ID or channel name where the message exists | | `timestamp` | string | Yes | Timestamp of the message to pin | ## `slack_send_message` [Section titled “slack\_send\_message”](#slack_send_message) Sends a message to a Slack channel or direct message. Requires a valid Slack OAuth2 connection with chat:write scope. | Name | Type | Required | Description | | ----------------- | ------- | -------- | -------------------------------------------------------------------------- | | `attachments` | string | No | JSON-encoded array of attachment objects for additional message formatting | | `blocks` | string | No | JSON-encoded array of Block Kit block elements for rich message formatting | | `channel` | string | Yes | Channel ID, channel name (#general), or user ID for DM | | `reply_broadcast` | boolean | No | Used in conjunction with thread\_ts to broadcast reply to channel | | `schema_version` | string | No | Optional schema version to use for tool execution | | `text` | string | Yes | Message text content | | `thread_ts` | string | No | Timestamp of parent message to reply in thread | | `tool_version` | string | No | Optional tool version to use for execution | | `unfurl_links` | boolean | No | Enable or disable link previews | | `unfurl_media` | boolean | No | Enable or disable media link previews | ## `slack_set_user_status` [Section titled “slack\_set\_user\_status”](#slack_set_user_status) Set the user’s custom status with text and emoji. This appears in their profile and can include an expiration time. Requires a valid Slack OAuth2 connection with users.profile:write scope. | Name | Type | Required | Description | | ------------------- | ------- | -------- | --------------------------------------------- | | `status_emoji` | string | No | Emoji to display with status (without colons) | | `status_expiration` | integer | No | Unix timestamp when status should expire | | `status_text` | string | No | Status text to display | ## `slack_update_message` [Section titled “slack\_update\_message”](#slack_update_message) Updates/edits a previously sent message in a Slack channel or direct message. Requires a valid Slack OAuth2 connection with chat:write scope. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------------------------------------------------- | | `attachments` | string | No | JSON-encoded array of attachment objects for additional message formatting | | `blocks` | string | No | JSON-encoded array of Block Kit block elements for rich message formatting | | `channel` | string | Yes | Channel ID, channel name (#general), or user ID for DM where the message was sent | | `text` | string | No | New message text content | | `ts` | string | Yes | Timestamp of the message to update | --- # DOCUMENT BOUNDARY --- # Snowflake > Connect to Snowflake to manage and analyze your data warehouse workloads Connect to Snowflake to manage and analyze your data warehouse workloads ![Snowflake logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/snowflake.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Snowflake connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. You’ll need to create an OAuth Security Integration in your Snowflake account. 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. * Find **Snowflake** from the list of providers and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.BKGB8xbb.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * Log into your Snowflake account (Snowsight) and run the following SQL to create an OAuth Security Integration, replacing `` with the URI you copied: ```sql 1 CREATE OR REPLACE SECURITY INTEGRATION scalekit_oauth 2 TYPE = OAUTH 3 OAUTH_CLIENT = CUSTOM 4 OAUTH_CLIENT_TYPE = 'CONFIDENTIAL' 5 OAUTH_REDIRECT_URI = '' 6 ENABLED = TRUE; ``` 2. ### Get client credentials * After creating the integration, run the following SQL to retrieve the client credentials: ```sql 1 SELECT SYSTEM$SHOW_OAUTH_CLIENT_SECRETS('SCALEKIT_OAUTH'); ``` * This returns a JSON object containing: * **Client ID** — value of `OAUTH_CLIENT_ID` * **Client Secret** — value of `OAUTH_CLIENT_SECRET_2` (or `OAUTH_CLIENT_SECRET_1`) 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from the SQL output) * Client Secret (from the SQL output) ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Snowflake account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. **Don’t worry about your Snowflake account domain in the path.** Scalekit automatically resolves `{{domain}}` from the connected account’s configuration. For example, a request with `path="/api/v2/statements"` will be sent to `https://myorg-myaccount.snowflakecomputing.com/api/v2/statements` automatically. You can interact with Snowflake in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'snowflake'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Snowflake:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/api/v2/statements', 29 method: 'POST', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "snowflake" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Snowflake:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/api/v2/statements", 30 method="POST" 31 ) 32 print(result) ``` ## Scalekit Tools ## Tool list [Section titled “Tool list”](#tool-list) ## `snowflake_cancel_query` [Section titled “snowflake\_cancel\_query”](#snowflake_cancel_query) Cancel a running Snowflake SQL API statement by statement handle. | Name | Type | Required | Description | | ------------------ | ------ | -------- | --------------------------------------------------------- | | `request_id` | string | No | Optional request ID used when the statement was submitted | | `statement_handle` | string | Yes | Snowflake statement handle to cancel | ## `snowflake_execute_query` [Section titled “snowflake\_execute\_query”](#snowflake_execute_query) Execute one or more SQL statements against Snowflake using the SQL API. Requires a valid Snowflake OAuth2 connection. Use semicolons to submit multiple statements. | Name | Type | Required | Description | | ------------ | -------- | -------- | ------------------------------------------------------------------------------------ | | `async` | boolean | No | Execute statement asynchronously and return a statement handle | | `bindings` | `object` | No | Bind variables object for ’?’ placeholders in the SQL statement | | `database` | string | No | Database to use when executing the statement | | `nullable` | boolean | No | When false, SQL NULL values are returned as the string “null” | | `parameters` | `object` | No | Statement-level Snowflake parameters as a JSON object | | `request_id` | string | No | Unique request identifier (UUID) used for idempotent retries | | `retry` | boolean | No | Set true when resubmitting a previously sent request with the same request\_id | | `role` | string | No | Role to use when executing the statement | | `schema` | string | No | Schema to use when executing the statement | | `statement` | string | Yes | SQL statement to execute. Use semicolons to send multiple statements in one request. | | `timeout` | integer | No | Maximum number of seconds to wait for statement execution | | `warehouse` | string | No | Warehouse to use when executing the statement | ## `snowflake_get_columns` [Section titled “snowflake\_get\_columns”](#snowflake_get_columns) Query INFORMATION\_SCHEMA.COLUMNS for column metadata. | Name | Type | Required | Description | | ------------------ | ------- | -------- | ---------------------------- | | `column_name_like` | string | No | Optional column name pattern | | `database` | string | Yes | Database name | | `limit` | integer | No | Maximum rows | | `role` | string | No | Optional role | | `schema` | string | No | Optional schema filter | | `table` | string | No | Optional table filter | | `warehouse` | string | No | Optional warehouse | ## `snowflake_get_query_partition` [Section titled “snowflake\_get\_query\_partition”](#snowflake_get_query_partition) Get a specific result partition for a Snowflake SQL API statement. | Name | Type | Required | Description | | ------------------ | ------- | -------- | --------------------------------------------------------- | | `partition` | integer | Yes | Partition index to fetch (0-based) | | `request_id` | string | No | Optional request ID used when the statement was submitted | | `statement_handle` | string | Yes | Snowflake statement handle returned by Execute Query | ## `snowflake_get_query_status` [Section titled “snowflake\_get\_query\_status”](#snowflake_get_query_status) Get Snowflake SQL API statement status and first partition result metadata by statement handle. | Name | Type | Required | Description | | ------------------ | ------ | -------- | --------------------------------------------------------- | | `request_id` | string | No | Optional request ID used when the statement was submitted | | `statement_handle` | string | Yes | Snowflake statement handle returned by Execute Query | ## `snowflake_get_referential_constraints` [Section titled “snowflake\_get\_referential\_constraints”](#snowflake_get_referential_constraints) Query INFORMATION\_SCHEMA.REFERENTIAL\_CONSTRAINTS. | Name | Type | Required | Description | | ----------- | ------- | -------- | ---------------------- | | `database` | string | Yes | Database name | | `limit` | integer | No | Maximum rows | | `role` | string | No | Optional role | | `schema` | string | No | Optional schema filter | | `table` | string | No | Optional table filter | | `warehouse` | string | No | Optional warehouse | ## `snowflake_get_schemata` [Section titled “snowflake\_get\_schemata”](#snowflake_get_schemata) Query INFORMATION\_SCHEMA.SCHEMATA for schema metadata. | Name | Type | Required | Description | | ------------- | ------- | -------- | ----------------------- | | `database` | string | Yes | Database name | | `limit` | integer | No | Maximum rows | | `role` | string | No | Optional role | | `schema_like` | string | No | Optional schema pattern | | `warehouse` | string | No | Optional warehouse | ## `snowflake_get_table_constraints` [Section titled “snowflake\_get\_table\_constraints”](#snowflake_get_table_constraints) Query INFORMATION\_SCHEMA.TABLE\_CONSTRAINTS. | Name | Type | Required | Description | | ----------------- | ------- | -------- | ------------------------------- | | `constraint_type` | string | No | Optional constraint type filter | | `database` | string | Yes | Database name | | `limit` | integer | No | Maximum rows | | `role` | string | No | Optional role | | `schema` | string | No | Optional schema filter | | `table` | string | No | Optional table filter | | `warehouse` | string | No | Optional warehouse | ## `snowflake_get_tables` [Section titled “snowflake\_get\_tables”](#snowflake_get_tables) Query INFORMATION\_SCHEMA.TABLES for table metadata in a Snowflake database. | Name | Type | Required | Description | | ----------------- | ------- | -------- | --------------------------- | | `database` | string | Yes | Database name | | `limit` | integer | No | Maximum number of rows | | `role` | string | No | Optional role | | `schema` | string | No | Optional schema filter | | `table_name_like` | string | No | Optional table name pattern | | `warehouse` | string | No | Optional warehouse | ## `snowflake_show_databases_schemas` [Section titled “snowflake\_show\_databases\_schemas”](#snowflake_show_databases_schemas) Run SHOW DATABASES or SHOW SCHEMAS. | Name | Type | Required | Description | | --------------- | ------ | -------- | ---------------------------------------- | | `database_name` | string | No | Optional database scope for SHOW SCHEMAS | | `like_pattern` | string | No | Optional LIKE pattern | | `object_type` | string | Yes | Object type to show | | `role` | string | No | Optional role | | `warehouse` | string | No | Optional warehouse | ## `snowflake_show_grants` [Section titled “snowflake\_show\_grants”](#snowflake_show_grants) Run SHOW GRANTS in common modes (to role, to user, of role, on object). | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------- | | `grant_view` | string | Yes | SHOW GRANTS variant | | `object_name` | string | No | Object name for on\_object | | `object_type` | string | No | Object type for on\_object | | `role` | string | No | Optional execution role | | `role_name` | string | No | Role name (for to\_role/of\_role) | | `user_name` | string | No | User name (for to\_user) | | `warehouse` | string | No | Optional warehouse | ## `snowflake_show_imported_exported_keys` [Section titled “snowflake\_show\_imported\_exported\_keys”](#snowflake_show_imported_exported_keys) Run SHOW IMPORTED KEYS or SHOW EXPORTED KEYS for a table. For reliable execution in this environment, use fully-qualified scope (database\_name + schema\_name + table\_name). | Name | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------------- | | `database_name` | string | No | Optional database name (recommended with schema\_name) | | `key_direction` | string | Yes | Which command to run | | `role` | string | No | Optional role | | `schema_name` | string | No | Optional schema name (recommended with database\_name) | | `table_name` | string | Yes | Table name (use with schema\_name and database\_name for fully-qualified scope) | | `warehouse` | string | No | Optional warehouse | ## `snowflake_show_primary_keys` [Section titled “snowflake\_show\_primary\_keys”](#snowflake_show_primary_keys) Run SHOW PRIMARY KEYS with optional scope. When using schema\_name (or schema\_name + table\_name), database\_name is required for fully-qualified scope. | Name | Type | Required | Description | | --------------- | ------ | -------- | -------------------------------------------------------------------- | | `database_name` | string | No | Optional database name for scope (required when schema\_name is set) | | `role` | string | No | Optional role | | `schema_name` | string | No | Optional schema name for scope | | `table_name` | string | No | Optional table name for scope | | `warehouse` | string | No | Optional warehouse | ## `snowflake_show_warehouses` [Section titled “snowflake\_show\_warehouses”](#snowflake_show_warehouses) Run SHOW WAREHOUSES. | Name | Type | Required | Description | | -------------- | ------ | -------- | --------------------- | | `like_pattern` | string | No | Optional LIKE pattern | | `role` | string | No | Optional role | | `warehouse` | string | No | Optional warehouse | --- # DOCUMENT BOUNDARY --- # Snowflake Key Pair Auth > Connect to Snowflake via Public Private Key Pair to manage and analyze your data warehouse workloads Connect to Snowflake via Public Private Key Pair to manage and analyze your data warehouse workloads ![Snowflake Key Pair Auth logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/snowflake.svg) Supports authentication: Bearer Token ## Tool list [Section titled “Tool list”](#tool-list) ## `snowflakekeyauth_cancel_query` [Section titled “snowflakekeyauth\_cancel\_query”](#snowflakekeyauth_cancel_query) Cancel a running Snowflake SQL API statement by statement handle. | Name | Type | Required | Description | | ------------------ | ------ | -------- | --------------------------------------------------------- | | `request_id` | string | No | Optional request ID used when the statement was submitted | | `statement_handle` | string | Yes | Snowflake statement handle to cancel | ## `snowflakekeyauth_execute_query` [Section titled “snowflakekeyauth\_execute\_query”](#snowflakekeyauth_execute_query) Execute one or more SQL statements against Snowflake using the SQL API. Requires a valid Snowflake OAuth2 connection. Use semicolons to submit multiple statements. | Name | Type | Required | Description | | ------------ | -------- | -------- | ------------------------------------------------------------------------------------ | | `async` | boolean | No | Execute statement asynchronously and return a statement handle | | `bindings` | `object` | No | Bind variables object for ’?’ placeholders in the SQL statement | | `database` | string | No | Database to use when executing the statement | | `nullable` | boolean | No | When false, SQL NULL values are returned as the string “null” | | `parameters` | `object` | No | Statement-level Snowflake parameters as a JSON object | | `request_id` | string | No | Unique request identifier (UUID) used for idempotent retries | | `retry` | boolean | No | Set true when resubmitting a previously sent request with the same request\_id | | `role` | string | No | Role to use when executing the statement | | `schema` | string | No | Schema to use when executing the statement | | `statement` | string | Yes | SQL statement to execute. Use semicolons to send multiple statements in one request. | | `timeout` | integer | No | Maximum number of seconds to wait for statement execution | | `warehouse` | string | No | Warehouse to use when executing the statement | ## `snowflakekeyauth_get_columns` [Section titled “snowflakekeyauth\_get\_columns”](#snowflakekeyauth_get_columns) Query INFORMATION\_SCHEMA.COLUMNS for column metadata. | Name | Type | Required | Description | | ------------------ | ------- | -------- | ---------------------------- | | `column_name_like` | string | No | Optional column name pattern | | `database` | string | Yes | Database name | | `limit` | integer | No | Maximum rows | | `role` | string | No | Optional role | | `schema` | string | No | Optional schema filter | | `table` | string | No | Optional table filter | | `warehouse` | string | No | Optional warehouse | ## `snowflakekeyauth_get_query_partition` [Section titled “snowflakekeyauth\_get\_query\_partition”](#snowflakekeyauth_get_query_partition) Get a specific result partition for a Snowflake SQL API statement. | Name | Type | Required | Description | | ------------------ | ------- | -------- | --------------------------------------------------------- | | `partition` | integer | Yes | Partition index to fetch (0-based) | | `request_id` | string | No | Optional request ID used when the statement was submitted | | `statement_handle` | string | Yes | Snowflake statement handle returned by Execute Query | ## `snowflakekeyauth_get_query_status` [Section titled “snowflakekeyauth\_get\_query\_status”](#snowflakekeyauth_get_query_status) Get Snowflake SQL API statement status and first partition result metadata by statement handle. | Name | Type | Required | Description | | ------------------ | ------ | -------- | --------------------------------------------------------- | | `request_id` | string | No | Optional request ID used when the statement was submitted | | `statement_handle` | string | Yes | Snowflake statement handle returned by Execute Query | ## `snowflakekeyauth_get_referential_constraints` [Section titled “snowflakekeyauth\_get\_referential\_constraints”](#snowflakekeyauth_get_referential_constraints) Query INFORMATION\_SCHEMA.REFERENTIAL\_CONSTRAINTS. | Name | Type | Required | Description | | ----------- | ------- | -------- | ---------------------- | | `database` | string | Yes | Database name | | `limit` | integer | No | Maximum rows | | `role` | string | No | Optional role | | `schema` | string | No | Optional schema filter | | `table` | string | No | Optional table filter | | `warehouse` | string | No | Optional warehouse | ## `snowflakekeyauth_get_schemata` [Section titled “snowflakekeyauth\_get\_schemata”](#snowflakekeyauth_get_schemata) Query INFORMATION\_SCHEMA.SCHEMATA for schema metadata. | Name | Type | Required | Description | | ------------- | ------- | -------- | ----------------------- | | `database` | string | Yes | Database name | | `limit` | integer | No | Maximum rows | | `role` | string | No | Optional role | | `schema_like` | string | No | Optional schema pattern | | `warehouse` | string | No | Optional warehouse | ## `snowflakekeyauth_get_table_constraints` [Section titled “snowflakekeyauth\_get\_table\_constraints”](#snowflakekeyauth_get_table_constraints) Query INFORMATION\_SCHEMA.TABLE\_CONSTRAINTS. | Name | Type | Required | Description | | ----------------- | ------- | -------- | ------------------------------- | | `constraint_type` | string | No | Optional constraint type filter | | `database` | string | Yes | Database name | | `limit` | integer | No | Maximum rows | | `role` | string | No | Optional role | | `schema` | string | No | Optional schema filter | | `table` | string | No | Optional table filter | | `warehouse` | string | No | Optional warehouse | ## `snowflakekeyauth_get_tables` [Section titled “snowflakekeyauth\_get\_tables”](#snowflakekeyauth_get_tables) Query INFORMATION\_SCHEMA.TABLES for table metadata in a Snowflake database. | Name | Type | Required | Description | | ----------------- | ------- | -------- | --------------------------- | | `database` | string | Yes | Database name | | `limit` | integer | No | Maximum number of rows | | `role` | string | No | Optional role | | `schema` | string | No | Optional schema filter | | `table_name_like` | string | No | Optional table name pattern | | `warehouse` | string | No | Optional warehouse | ## `snowflakekeyauth_show_databases_schemas` [Section titled “snowflakekeyauth\_show\_databases\_schemas”](#snowflakekeyauth_show_databases_schemas) Run SHOW DATABASES or SHOW SCHEMAS. | Name | Type | Required | Description | | --------------- | ------ | -------- | ---------------------------------------- | | `database_name` | string | No | Optional database scope for SHOW SCHEMAS | | `like_pattern` | string | No | Optional LIKE pattern | | `object_type` | string | Yes | Object type to show | | `role` | string | No | Optional role | | `warehouse` | string | No | Optional warehouse | ## `snowflakekeyauth_show_grants` [Section titled “snowflakekeyauth\_show\_grants”](#snowflakekeyauth_show_grants) Run SHOW GRANTS in common modes (to role, to user, of role, on object). | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------- | | `grant_view` | string | Yes | SHOW GRANTS variant | | `object_name` | string | No | Object name for on\_object | | `object_type` | string | No | Object type for on\_object | | `role` | string | No | Optional execution role | | `role_name` | string | No | Role name (for to\_role/of\_role) | | `user_name` | string | No | User name (for to\_user) | | `warehouse` | string | No | Optional warehouse | ## `snowflakekeyauth_show_imported_exported_keys` [Section titled “snowflakekeyauth\_show\_imported\_exported\_keys”](#snowflakekeyauth_show_imported_exported_keys) Run SHOW IMPORTED KEYS or SHOW EXPORTED KEYS for a table. For reliable execution in this environment, use fully-qualified scope (database\_name + schema\_name + table\_name). | Name | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------------- | | `database_name` | string | No | Optional database name (recommended with schema\_name) | | `key_direction` | string | Yes | Which command to run | | `role` | string | No | Optional role | | `schema_name` | string | No | Optional schema name (recommended with database\_name) | | `table_name` | string | Yes | Table name (use with schema\_name and database\_name for fully-qualified scope) | | `warehouse` | string | No | Optional warehouse | ## `snowflakekeyauth_show_primary_keys` [Section titled “snowflakekeyauth\_show\_primary\_keys”](#snowflakekeyauth_show_primary_keys) Run SHOW PRIMARY KEYS with optional scope. When using schema\_name (or schema\_name + table\_name), database\_name is required for fully-qualified scope. | Name | Type | Required | Description | | --------------- | ------ | -------- | -------------------------------------------------------------------- | | `database_name` | string | No | Optional database name for scope (required when schema\_name is set) | | `role` | string | No | Optional role | | `schema_name` | string | No | Optional schema name for scope | | `table_name` | string | No | Optional table name for scope | | `warehouse` | string | No | Optional warehouse | ## `snowflakekeyauth_show_warehouses` [Section titled “snowflakekeyauth\_show\_warehouses”](#snowflakekeyauth_show_warehouses) Run SHOW WAREHOUSES. | Name | Type | Required | Description | | -------------- | ------ | -------- | --------------------- | | `like_pattern` | string | No | Optional LIKE pattern | | `role` | string | No | Optional role | | `warehouse` | string | No | Optional warehouse | --- # DOCUMENT BOUNDARY --- # Trello > Connect to Trello. Manage boards, cards, lists, and team collaboration workflows Connect to Trello. Manage boards, cards, lists, and team collaboration workflows ![Trello logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/trello_n.svg) Supports authentication: OAuth 1.0a ## Usage [Section titled “Usage”](#usage) Connect a user’s Trello account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Trello in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'trello'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Trello:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/1/members/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "trello" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Trello:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/1/members/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Vimeo > Connect to Vimeo API v3.4. Upload and manage videos, organize content into showcases and folders, manage channels, handle comments, likes, and webhooks. Connect to Vimeo API v3.4. Upload and manage videos, organize content into showcases and folders, manage channels, handle comments, likes, and webhooks. ![Vimeo logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/vimeo.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Vimeo app credentials with Scalekit so it can manage the OAuth 2.0 authentication flow and token lifecycle on your behalf. You’ll need a Client Identifier and Client Secret from the [Vimeo Developer Portal](https://developer.vimeo.com/). 1. ### Create a Vimeo app * Go to the [Vimeo Developer Portal](https://developer.vimeo.com/) and click **Create an app** in the top-right corner. * Fill in the required fields: * **App name** — enter a name for your app (e.g., `Scalekit-Auth`) * **Brief description** — describe what the app does * Select **Yes** under “Will people besides you be able to access your app?” to allow other Vimeo accounts to authenticate * Check the box to agree to the Vimeo API License Agreement and Terms of Service ![Vimeo Create a new app form filled with Scalekit-Auth details](/_astro/create-app-filled.DTehtCq-.png) * Click **Create App**. 2. ### Copy your Client Identifier After creating the app, you are taken to the app’s settings page. Copy the **Client identifier** — you’ll need it in a later step. ![Vimeo app settings page showing the Client Identifier](/_astro/app-client-identifier.WChWoe_o.png) 3. ### Create a connection in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Search for **Vimeo** and click **Create**. ![Searching for Vimeo in Scalekit Create Connection](/_astro/scalekit-search-vimeo.DMKl4jg-.png) * Copy the **Redirect URI** from the connection configuration panel. It looks like `https:///sso/v1/oauth//callback`. ![Configure Vimeo Connection panel showing Redirect URI, Client ID, Client Secret, and Scopes fields](/_astro/configure-vimeo-connection.BHvYPNjT.png) 4. ### Configure the callback URL in Vimeo * Back in the [Vimeo Developer Portal](https://developer.vimeo.com/), open your app and click **Edit settings**. * Paste the Scalekit Redirect URI into the **App URL** field and the **Your callback URLs** field. * Click **Add secret** under **Client secrets** to generate a new client secret. Copy the secret value. ![Vimeo app settings with callback URL and client secret configured](/_astro/vimeo-app-callback-url.S3qiRtuo.png) * Click **Update** to save. 5. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the Vimeo connection you created. * Enter your credentials: * **Client ID** — the Client Identifier from your Vimeo app * **Client Secret** — the secret you generated in step 4 * **Scopes** — select the scopes your app needs (e.g., `create`, `delete`, `edit`, `interact`, `private`, `public`) * Click **Save**. ## Tool list [Section titled “Tool list”](#tool-list) ## `vimeo_categories_list` [Section titled “vimeo\_categories\_list”](#vimeo_categories_list) Retrieve all top-level Vimeo content categories (e.g., Animation, Documentary, Music). Requires public scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | ----------------------------- | | `direction` | string | No | Sort direction | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of categories per page | | `sort` | string | No | Sort order for categories | ## `vimeo_channel_videos_list` [Section titled “vimeo\_channel\_videos\_list”](#vimeo_channel_videos_list) Retrieve all videos in a specific Vimeo channel. Requires public scope. | Name | Type | Required | Description | | ------------ | ------- | -------- | ------------------------------------- | | `channel_id` | string | Yes | Vimeo channel ID or slug | | `direction` | string | No | Sort direction | | `filter` | string | No | Filter videos by type | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of videos per page | | `query` | string | No | Search query to filter channel videos | | `sort` | string | No | Sort order for videos | ## `vimeo_channels_list` [Section titled “vimeo\_channels\_list”](#vimeo_channels_list) Retrieve a list of Vimeo channels. Can list all public channels or channels the authenticated user follows/manages. Requires public scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | --------------------------------------- | | `direction` | string | No | Sort direction | | `filter` | string | No | Filter channels by type | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of channels per page | | `query` | string | No | Search query to filter channels by name | | `sort` | string | No | Sort order for channels | ## `vimeo_folder_create` [Section titled “vimeo\_folder\_create”](#vimeo_folder_create) Create a new folder (project) in the authenticated user’s Vimeo account for organizing private video content. Requires create scope. | Name | Type | Required | Description | | ------------------- | ------ | -------- | --------------------------------------------------- | | `name` | string | Yes | Name of the new folder | | `parent_folder_uri` | string | No | URI of the parent folder to nest this folder inside | ## `vimeo_folder_video_add` [Section titled “vimeo\_folder\_video\_add”](#vimeo_folder_video_add) Move or add a video into a Vimeo folder (project). Requires edit scope. | Name | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------- | | `folder_id` | string | Yes | Folder (project) ID to add the video to | | `video_id` | string | Yes | Video ID to add to the folder | ## `vimeo_folder_videos_list` [Section titled “vimeo\_folder\_videos\_list”](#vimeo_folder_videos_list) Retrieve all videos inside a specific Vimeo folder (project). Requires private scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | --------------------------------------- | | `direction` | string | No | Sort direction | | `filter` | string | No | Filter videos by type | | `folder_id` | string | Yes | Folder (project) ID to list videos from | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of videos per page | | `query` | string | No | Search query to filter videos by name | | `sort` | string | No | Sort order for videos | ## `vimeo_folders_list` [Section titled “vimeo\_folders\_list”](#vimeo_folders_list) Retrieve all folders (projects) owned by the authenticated Vimeo user for organizing private video libraries. Requires private scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | -------------------------------------- | | `direction` | string | No | Sort direction | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of folders per page | | `query` | string | No | Search query to filter folders by name | | `sort` | string | No | Sort order for folders | ## `vimeo_following_list` [Section titled “vimeo\_following\_list”](#vimeo_following_list) Retrieve a list of Vimeo users that the authenticated user is following. Requires private scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | --------------------------------------------- | | `direction` | string | No | Sort direction | | `filter` | string | No | Filter following list by type | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of users per page | | `query` | string | No | Search query to filter following list by name | | `sort` | string | No | Sort order | ## `vimeo_liked_videos_list` [Section titled “vimeo\_liked\_videos\_list”](#vimeo_liked_videos_list) Retrieve all videos liked by the authenticated Vimeo user. Requires private scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | --------------------------- | | `direction` | string | No | Sort direction | | `filter` | string | No | Filter liked videos by type | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of videos per page | | `sort` | string | No | Sort order for liked videos | ## `vimeo_me_get` [Section titled “vimeo\_me\_get”](#vimeo_me_get) Retrieve the authenticated Vimeo user’s profile including account type, bio, location, stats, and links. Requires a valid Vimeo OAuth2 connection. ## `vimeo_my_videos_list` [Section titled “vimeo\_my\_videos\_list”](#vimeo_my_videos_list) Retrieve all videos uploaded by the authenticated Vimeo user. Supports filtering, sorting, and pagination. Requires private scope. | Name | Type | Required | Description | | ---------------- | ------- | -------- | ----------------------------------------------------- | | `containing_uri` | string | No | Filter videos that contain a specific URI | | `direction` | string | No | Sort direction | | `filter` | string | No | Filter videos by type | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of videos per page | | `query` | string | No | Search query to filter videos by title or description | | `sort` | string | No | Sort order for video results | ## `vimeo_showcase_create` [Section titled “vimeo\_showcase\_create”](#vimeo_showcase_create) Create a new showcase (album) on Vimeo for organizing videos. Supports privacy, password protection, branding, and embed settings. Requires create scope. | Name | Type | Required | Description | | --------------- | ------- | -------- | ----------------------------------------------------------- | | `brand_color` | string | No | Hex color code for showcase branding | | `description` | string | No | Description of the showcase | | `hide_nav` | boolean | No | Whether to hide Vimeo navigation in the showcase | | `hide_upcoming` | boolean | No | Whether to hide upcoming live events in the showcase | | `name` | string | Yes | Name/title of the showcase | | `password` | string | No | Password for the showcase when privacy is set to ‘password’ | | `privacy` | string | No | Privacy setting for the showcase | | `review_mode` | boolean | No | Enable review mode for the showcase | | `sort` | string | No | Default sort for videos in the showcase | ## `vimeo_showcase_video_add` [Section titled “vimeo\_showcase\_video\_add”](#vimeo_showcase_video_add) Add a video to a Vimeo showcase. Requires edit scope and ownership of both the showcase and the video. | Name | Type | Required | Description | | ---------- | ------ | -------- | --------------------------------------- | | `album_id` | string | Yes | Showcase (album) ID to add the video to | | `video_id` | string | Yes | Video ID to add to the showcase | ## `vimeo_showcase_videos_list` [Section titled “vimeo\_showcase\_videos\_list”](#vimeo_showcase_videos_list) Retrieve all videos in a specific Vimeo showcase. Requires private scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | ------------------------- | | `album_id` | string | Yes | Showcase (album) ID | | `direction` | string | No | Sort direction | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of videos per page | | `sort` | string | No | Sort order for videos | ## `vimeo_showcases_list` [Section titled “vimeo\_showcases\_list”](#vimeo_showcases_list) Retrieve all showcases (formerly albums) owned by the authenticated Vimeo user. Requires private scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | ---------------------------------------- | | `direction` | string | No | Sort direction | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of showcases per page | | `query` | string | No | Search query to filter showcases by name | | `sort` | string | No | Sort order for showcases | ## `vimeo_user_follow` [Section titled “vimeo\_user\_follow”](#vimeo_user_follow) Follow a Vimeo user on behalf of the authenticated user. Requires interact scope. | Name | Type | Required | Description | | ---------------- | ------ | -------- | ----------------------- | | `follow_user_id` | string | Yes | Vimeo user ID to follow | ## `vimeo_user_get` [Section titled “vimeo\_user\_get”](#vimeo_user_get) Retrieve public profile information for any Vimeo user by their user ID or username. Requires public scope. | Name | Type | Required | Description | | --------- | ------ | -------- | ------------------------- | | `user_id` | string | Yes | Vimeo user ID or username | ## `vimeo_user_videos_list` [Section titled “vimeo\_user\_videos\_list”](#vimeo_user_videos_list) Retrieve all public videos uploaded by a specific Vimeo user. Supports filtering and pagination. Requires public scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | ----------------------------- | | `direction` | string | No | Sort direction | | `filter` | string | No | Filter results by video type | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of videos per page | | `query` | string | No | Search query to filter videos | | `sort` | string | No | Sort order for video results | | `user_id` | string | Yes | Vimeo user ID or username | ## `vimeo_video_comment_add` [Section titled “vimeo\_video\_comment\_add”](#vimeo_video_comment_add) Post a comment on a Vimeo video on behalf of the authenticated user. Requires interact scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------- | | `text` | string | Yes | Comment text to post | | `video_id` | string | Yes | Vimeo video ID to comment on | ## `vimeo_video_comments_list` [Section titled “vimeo\_video\_comments\_list”](#vimeo_video_comments_list) Retrieve all comments posted on a specific Vimeo video. Requires public scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | ------------------------------------ | | `direction` | string | No | Sort direction | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of comments per page | | `video_id` | string | Yes | Vimeo video ID to list comments from | ## `vimeo_video_delete` [Section titled “vimeo\_video\_delete”](#vimeo_video_delete) Permanently delete a Vimeo video. This action is irreversible. Requires delete scope and ownership of the video. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------ | | `video_id` | string | Yes | Vimeo video ID to delete | ## `vimeo_video_edit` [Section titled “vimeo\_video\_edit”](#vimeo_video_edit) Update the metadata of an existing Vimeo video including title, description, privacy settings, tags, and content rating. Requires edit scope. | Name | Type | Required | Description | | ------------------ | ------- | -------- | -------------------------------------------------------------- | | `content_rating` | string | No | Content rating of the video | | `description` | string | No | New description for the video | | `license` | string | No | Creative Commons license to apply | | `name` | string | No | New title for the video | | `password` | string | No | Password for the video when privacy view is set to ‘password’ | | `privacy_add` | boolean | No | Whether users can add the video to their showcases or channels | | `privacy_comments` | string | No | Who can comment on the video | | `privacy_download` | boolean | No | Whether users can download the video | | `privacy_embed` | string | No | Who can embed the video | | `privacy_view` | string | No | Who can view the video | | `video_id` | string | Yes | Vimeo video ID to edit | ## `vimeo_video_get` [Section titled “vimeo\_video\_get”](#vimeo_video_get) Retrieve detailed information about a specific Vimeo video including metadata, privacy settings, stats, and embed details. Requires a valid Vimeo OAuth2 connection. | Name | Type | Required | Description | | ---------- | ------ | -------- | -------------- | | `video_id` | string | Yes | Vimeo video ID | ## `vimeo_video_like` [Section titled “vimeo\_video\_like”](#vimeo_video_like) Like a Vimeo video on behalf of the authenticated user. Use PUT /me/likes/\{video\_id\} to like. Requires interact scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | ---------------------- | | `video_id` | string | Yes | Vimeo video ID to like | ## `vimeo_video_tags_list` [Section titled “vimeo\_video\_tags\_list”](#vimeo_video_tags_list) Retrieve all tags applied to a specific Vimeo video. Requires public scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | -------------------------------- | | `video_id` | string | Yes | Vimeo video ID to list tags from | ## `vimeo_videos_search` [Section titled “vimeo\_videos\_search”](#vimeo_videos_search) Search for public videos on Vimeo using keywords and filters. Returns paginated video results with metadata. Requires a valid Vimeo OAuth2 connection with public scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | ------------------------------------ | | `direction` | string | No | Sort direction for results | | `filter` | string | No | Filter results by video type | | `page` | integer | No | Page number of results to return | | `per_page` | integer | No | Number of results to return per page | | `query` | string | Yes | Search query keywords | | `sort` | string | No | Sort order for search results | ## `vimeo_watchlater_add` [Section titled “vimeo\_watchlater\_add”](#vimeo_watchlater_add) Add a video to the authenticated user’s Vimeo Watch Later queue. Requires interact scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------ | | `video_id` | string | Yes | Vimeo video ID to add to Watch Later | ## `vimeo_watchlater_list` [Section titled “vimeo\_watchlater\_list”](#vimeo_watchlater_list) Retrieve all videos in the authenticated user’s Vimeo Watch Later queue. Requires private scope. | Name | Type | Required | Description | | ----------- | ------- | -------- | --------------------------------- | | `direction` | string | No | Sort direction | | `filter` | string | No | Filter by video type | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of videos per page | | `sort` | string | No | Sort order for watch later videos | ## `vimeo_webhook_create` [Section titled “vimeo\_webhook\_create”](#vimeo_webhook_create) Register a new webhook endpoint to receive real-time Vimeo event notifications. Supports events for video uploads, transcoding, privacy changes, and comments. Requires private scope. | Name | Type | Required | Description | | ------------- | --------------- | -------- | ------------------------------------------------------- | | `event_types` | `array` | Yes | List of event types that will trigger this webhook | | `url` | string | Yes | HTTPS URL that Vimeo will send webhook POST requests to | ## `vimeo_webhook_delete` [Section titled “vimeo\_webhook\_delete”](#vimeo_webhook_delete) Delete a registered Vimeo webhook endpoint so it no longer receives event notifications. Requires private scope. | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------- | | `webhook_id` | string | Yes | Webhook ID to delete | ## `vimeo_webhooks_list` [Section titled “vimeo\_webhooks\_list”](#vimeo_webhooks_list) Retrieve all webhooks registered for the authenticated Vimeo application. Requires private scope. | Name | Type | Required | Description | | ---------- | ------- | -------- | --------------------------- | | `page` | integer | No | Page number of results | | `per_page` | integer | No | Number of webhooks per page | --- # DOCUMENT BOUNDARY --- # YouTube > Connect to YouTube to access channel details, analytics, and upload or manage videos via OAuth 2.0 Connect to YouTube to access channel details, analytics, and upload or manage videos via OAuth 2.0 ![YouTube logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/youtube.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Google OAuth 2.0 credentials with Scalekit so it can manage the OAuth 2.0 authentication flow and token lifecycle for YouTube on your behalf. You’ll need a Client ID and Client Secret from the [Google Cloud Console](https://console.cloud.google.com/). 1. ### Create an OAuth 2.0 client in Google Cloud * Go to the [Google Cloud Console](https://console.cloud.google.com/) and select your project (or create a new one). ![Google Cloud Console welcome page](/_astro/google-cloud-console.DiiPE1Jj.png) * Search for **OAuth** in the top search bar. Select **Credentials** under **APIs & Services**. ![Searching for OAuth in Google Cloud Console](/_astro/search-oauth.Cs2HEEQZ.png) * On the **Credentials** page, click **+ Create credentials** and select **OAuth client ID**. ![Google Cloud Credentials page showing OAuth 2.0 Client IDs](/_astro/credentials-page.CqgnbHHf.png) ![Create credentials dropdown with OAuth client ID option](/_astro/create-credentials-dropdown.BumuOuVq.png) * Set the **Application type** to **Web application** and enter a **Name** (e.g., `Scalekit`). ![Create OAuth client ID form with Web application type and Scalekit name](/_astro/create-oauth-client-id.B6WNcyfI.png) * Leave the **Authorized redirect URIs** section empty for now — you’ll add the Scalekit redirect URI in a later step. 2. ### Create a connection in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. ![Scalekit Agent Auth connections page](/_astro/scalekit-agent-auth.FC_mUO5S.png) * Search for **YouTube** and click **Create**. ![Searching for YouTube in Scalekit Create Connection](/_astro/scalekit-search-youtube.J8OvrK2m.png) * Copy the **Redirect URI** from the connection configuration panel. It looks like `https:///sso/v1/oauth//callback`. ![Configure YouTube Connection panel showing Redirect URI, Client ID, Client Secret, and Scopes fields](/_astro/configure-youtube-connection.DS09NNuK.png) 3. ### Configure the redirect URI in Google Cloud * Back in the [Google Cloud Console](https://console.cloud.google.com/), open your OAuth 2.0 client (or continue from step 1). * Under **Authorized redirect URIs**, click **+ Add URI** and paste the Scalekit Redirect URI. ![Google Cloud OAuth client with Scalekit redirect URI added](/_astro/google-redirect-uri.B0f6YURr.png) * Click **Create** (or **Save** if editing an existing client). A dialog displays your **Client ID** and **Client secret**. Copy both values. ![OAuth client created dialog showing Client ID and Client secret](/_astro/oauth-client-created.CxDg-CFH.png) 4. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the YouTube connection you created. * Enter your credentials: * **Client ID** — the Client ID from your Google OAuth 2.0 client * **Client Secret** — the Client secret from the dialog in step 3 * **Scopes** — select the scopes your app needs (e.g., `youtube.readonly`, `youtube`, `youtube.force-ssl`, `yt-analytics.readonly`) ![Configure YouTube Connection panel filled with Client ID and Client Secret](/_astro/scalekit-credentials-filled.DVi2y5_d.png) * Click **Save**. ## Tool list [Section titled “Tool list”](#tool-list) ## `youtube_analytics_group_create` [Section titled “youtube\_analytics\_group\_create”](#youtube_analytics_group_create) Create a YouTube Analytics group to organize videos, playlists, channels, or assets for collective analytics reporting. | Name | Type | Required | Description | | ---------------------------- | ------ | -------- | -------------------------------------------- | | `item_type` | string | Yes | Type of items the group will contain | | `on_behalf_of_content_owner` | string | No | Content owner ID. For content partners only. | | `title` | string | Yes | Title of the analytics group | ## `youtube_analytics_group_item_insert` [Section titled “youtube\_analytics\_group\_item\_insert”](#youtube_analytics_group_item_insert) Add a video, playlist, or channel to a YouTube Analytics group. | Name | Type | Required | Description | | ---------------------------- | ------ | -------- | --------------------------------------------------------- | | `group_id` | string | Yes | ID of the Analytics group to add the item to | | `on_behalf_of_content_owner` | string | No | Content owner ID. For content partners only. | | `resource_id` | string | Yes | ID of the resource (video ID, channel ID, or playlist ID) | | `resource_kind` | string | Yes | Type of the resource | ## `youtube_analytics_group_items_delete` [Section titled “youtube\_analytics\_group\_items\_delete”](#youtube_analytics_group_items_delete) Remove an item (video, channel, or playlist) from a YouTube Analytics group. | Name | Type | Required | Description | | ---------------------------- | ------ | -------- | ---------------------------------------------------------- | | `id` | string | Yes | ID of the group item to remove | | `on_behalf_of_content_owner` | string | No | Content owner ID on whose behalf the request is being made | ## `youtube_analytics_group_items_list` [Section titled “youtube\_analytics\_group\_items\_list”](#youtube_analytics_group_items_list) Retrieve a list of items (videos, playlists, channels, or assets) that belong to a YouTube Analytics group. | Name | Type | Required | Description | | ---------------------------- | ------ | -------- | ---------------------------------------------------------- | | `group_id` | string | Yes | ID of the group whose items to retrieve | | `on_behalf_of_content_owner` | string | No | Content owner ID on whose behalf the request is being made | ## `youtube_analytics_groups_delete` [Section titled “youtube\_analytics\_groups\_delete”](#youtube_analytics_groups_delete) Delete a YouTube Analytics group. This removes the group but does not delete the videos, channels, or playlists within it. | Name | Type | Required | Description | | ---------------------------- | ------ | -------- | ---------------------------------------------------------- | | `group_id` | string | Yes | ID of the Analytics group to delete | | `on_behalf_of_content_owner` | string | No | Content owner ID on whose behalf the request is being made | ## `youtube_analytics_groups_list` [Section titled “youtube\_analytics\_groups\_list”](#youtube_analytics_groups_list) Retrieve a list of YouTube Analytics groups for a channel or content owner. Specify either id or mine to filter results. | Name | Type | Required | Description | | ---------------------------- | ------- | -------- | --------------------------------------------------------------------------------------- | | `id` | string | No | Comma-separated list of group IDs to retrieve | | `mine` | boolean | No | If true, return only groups owned by the authenticated user. Required if id is not set. | | `on_behalf_of_content_owner` | string | No | Content owner ID on whose behalf the request is being made | | `page_token` | string | No | Token for retrieving the next page of results | ## `youtube_analytics_groups_update` [Section titled “youtube\_analytics\_groups\_update”](#youtube_analytics_groups_update) Update the title of an existing YouTube Analytics group. | Name | Type | Required | Description | | ---------------------------- | ------ | -------- | -------------------------------------------- | | `group_id` | string | Yes | ID of the Analytics group to update | | `on_behalf_of_content_owner` | string | No | Content owner ID. For content partners only. | | `title` | string | Yes | New title for the Analytics group | ## `youtube_analytics_query` [Section titled “youtube\_analytics\_query”](#youtube_analytics_query) Query YouTube Analytics data to retrieve metrics like views, watch time, subscribers, revenue, etc. for channels or content owners. | Name | Type | Required | Description | | --------------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------- | | `currency` | string | No | Currency for monetary metrics (ISO 4217 code, e.g., USD) | | `dimensions` | string | No | Comma-separated list of dimensions to group results by (e.g., day,country,video) | | `end_date` | string | Yes | End date for the analytics report in YYYY-MM-DD format | | `filters` | string | No | Filter expression to narrow results (e.g., country==US, video==VIDEO\_ID) | | `ids` | string | Yes | Channel or content owner ID. Format: channel==CHANNEL\_ID or contentOwner==CONTENT\_OWNER\_ID | | `include_historical_channel_data` | boolean | No | Include historical channel data recorded before the channel was linked to a content owner | | `max_results` | integer | No | Maximum number of rows to return in the response (maximum value: 200) | | `metrics` | string | Yes | Comma-separated list of metrics to retrieve (e.g., views,estimatedMinutesWatched,likes,subscribersGained) | | `sort` | string | No | Comma-separated list of columns to sort by. Prefix with - for descending order (e.g., -views) | | `start_date` | string | Yes | Start date for the analytics report in YYYY-MM-DD format | | `start_index` | integer | No | 1-based index of the first row to return (for pagination) | ## `youtube_captions_list` [Section titled “youtube\_captions\_list”](#youtube_captions_list) Retrieve a list of caption tracks for a YouTube video. The part parameter is fixed to ‘snippet’. Requires youtube.force-ssl scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------------------------------- | | `id` | string | No | Comma-separated list of caption track IDs to filter results | | `video_id` | string | Yes | ID of the video to list captions for | ## `youtube_channels_list` [Section titled “youtube\_channels\_list”](#youtube_channels_list) Retrieve information about one or more YouTube channels including subscriber count, video count, and channel metadata. You must provide exactly one filter: id, mine, for\_handle, for\_username, or managed\_by\_me. Requires a valid YouTube OAuth2 connection. | Name | Type | Required | Description | | --------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `for_handle` | string | No | YouTube channel handle to look up (e.g., @MrBeast). Use instead of id, mine, or for\_username. | | `for_username` | string | No | YouTube username of the channel to look up (legacy). Use instead of id, mine, or for\_handle. | | `id` | string | No | Comma-separated list of YouTube channel IDs. Use instead of mine, for\_handle, or for\_username. | | `managed_by_me` | boolean | No | Return channels managed by the authenticated user (content partners only). Use instead of id, mine, for\_handle, or for\_username. | | `max_results` | integer | No | Maximum number of results to return (0-50, default: 5) | | `mine` | boolean | No | Return the authenticated user’s channel. Use instead of id, for\_handle, or for\_username. | | `page_token` | string | No | Token for pagination | | `part` | string | Yes | Comma-separated list of channel resource parts to include in the response | ## `youtube_comment_threads_insert` [Section titled “youtube\_comment\_threads\_insert”](#youtube_comment_threads_insert) Post a new top-level comment on a YouTube video. Requires youtube.force-ssl scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------- | | `text` | string | Yes | Text of the comment | | `video_id` | string | Yes | ID of the video to comment on | ## `youtube_comment_threads_list` [Section titled “youtube\_comment\_threads\_list”](#youtube_comment_threads_list) Retrieve top-level comment threads for a YouTube video or channel. You must provide exactly one filter: video\_id, all\_threads\_related\_to\_channel\_id, or id. Each thread includes the top-level comment and optionally its replies. Requires a valid YouTube OAuth2 connection. | Name | Type | Required | Description | | ----------------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `all_threads_related_to_channel_id` | string | No | Return all comment threads associated with a specific channel. Use instead of video\_id or id. | | `id` | string | No | Comma-separated list of comment thread IDs to retrieve. Use instead of video\_id or all\_threads\_related\_to\_channel\_id. | | `max_results` | integer | No | Maximum number of comment threads to return (1-100, default: 20) | | `order` | string | No | Sort order for comment threads | | `page_token` | string | No | Token for pagination | | `part` | string | Yes | Comma-separated list of comment thread resource parts to include | | `search_terms` | string | No | Limit results to comments containing these search terms | | `video_id` | string | No | YouTube video ID to fetch comment threads for. Use instead of all\_threads\_related\_to\_channel\_id or id. | ## `youtube_comments_list` [Section titled “youtube\_comments\_list”](#youtube_comments_list) Retrieve a list of replies to a specific YouTube comment thread. You must provide exactly one filter: parent\_id or id. The part parameter is fixed to ‘snippet’. Requires youtube.readonly scope. | Name | Type | Required | Description | | ------------- | ------- | -------- | ----------------------------------------------------------------------------------------- | | `id` | string | No | Comma-separated list of comment IDs to retrieve. Use instead of parent\_id. | | `max_results` | integer | No | Maximum number of replies to return (1-100, default: 20). Cannot be used with id filter. | | `page_token` | string | No | Token for pagination to retrieve the next page of replies. Cannot be used with id filter. | | `parent_id` | string | No | ID of the comment thread (top-level comment) to list replies for. Use instead of id. | | `text_format` | string | No | Format of the comment text in the response | ## `youtube_playlist_delete` [Section titled “youtube\_playlist\_delete”](#youtube_playlist_delete) Permanently delete a YouTube playlist. This action cannot be undone. Requires youtube scope. | Name | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------- | | `playlist_id` | string | Yes | ID of the playlist to delete | ## `youtube_playlist_insert` [Section titled “youtube\_playlist\_insert”](#youtube_playlist_insert) Create a new YouTube playlist for the authenticated user. Requires youtube scope. | Name | Type | Required | Description | | ------------------ | --------------- | -------- | -------------------------------- | | `default_language` | string | No | Default language of the playlist | | `description` | string | No | Playlist description | | `privacy_status` | string | No | Privacy setting | | `tags` | `array` | No | Tags for the playlist | | `title` | string | Yes | Playlist title | ## `youtube_playlist_items_delete` [Section titled “youtube\_playlist\_items\_delete”](#youtube_playlist_items_delete) Remove a video from a YouTube playlist by its playlist item ID. Requires youtube scope. | Name | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------------------------------- | | `playlist_item_id` | string | Yes | ID of the playlist item to remove (not the video ID) | ## `youtube_playlist_items_insert` [Section titled “youtube\_playlist\_items\_insert”](#youtube_playlist_items_insert) Add a video to a YouTube playlist at an optional position. Requires youtube scope. | Name | Type | Required | Description | | ------------- | ------- | -------- | -------------------------------------------------------- | | `note` | string | No | Optional note for this playlist item | | `playlist_id` | string | Yes | Playlist to add the video to | | `position` | integer | No | Zero-based position in the playlist. Omit to add at end. | | `video_id` | string | Yes | YouTube video ID to add | ## `youtube_playlist_items_list` [Section titled “youtube\_playlist\_items\_list”](#youtube_playlist_items_list) Retrieve a list of videos in a YouTube playlist. Returns playlist items with video details, positions, and metadata. Requires a valid YouTube OAuth2 connection. | Name | Type | Required | Description | | ------------- | ------- | -------- | --------------------------------------------------------------- | | `max_results` | integer | No | Maximum number of playlist items to return (0-50, default: 5) | | `page_token` | string | No | Token for pagination to retrieve the next page | | `part` | string | Yes | Comma-separated list of playlist item resource parts to include | | `playlist_id` | string | Yes | YouTube playlist ID to retrieve items from | | `video_id` | string | No | Filter results to items containing a specific video | ## `youtube_playlist_update` [Section titled “youtube\_playlist\_update”](#youtube_playlist_update) Update an existing YouTube playlist’s title, description, privacy status, or default language. Requires youtube scope. | Name | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------- | | `default_language` | string | No | Language of the playlist | | `description` | string | No | New playlist description | | `playlist_id` | string | Yes | ID of the playlist to update | | `privacy_status` | string | No | New privacy setting | | `title` | string | No | New playlist title | ## `youtube_playlists_list` [Section titled “youtube\_playlists\_list”](#youtube_playlists_list) Retrieve a list of YouTube playlists for a channel or the authenticated user. You must provide exactly one filter: channel\_id, id, or mine. Requires a valid YouTube OAuth2 connection. | Name | Type | Required | Description | | ------------- | ------- | -------- | ------------------------------------------------------------------------------------- | | `channel_id` | string | No | Return playlists for a specific channel. Use instead of id or mine. | | `id` | string | No | Comma-separated list of playlist IDs to retrieve. Use instead of channel\_id or mine. | | `max_results` | integer | No | Maximum number of playlists to return (0-50, default: 5) | | `mine` | boolean | No | Return playlists owned by the authenticated user. Use instead of channel\_id or id. | | `page_token` | string | No | Token for pagination | | `part` | string | Yes | Comma-separated list of playlist resource parts to include | ## `youtube_reporting_create_job` [Section titled “youtube\_reporting\_create\_job”](#youtube_reporting_create_job) Create a YouTube reporting job to schedule daily generation of a specific report type. Once created, YouTube will generate the report daily. | Name | Type | Required | Description | | ---------------------------- | ------ | -------- | --------------------------------------------------------------------------------------- | | `name` | string | Yes | Human-readable name for the reporting job | | `on_behalf_of_content_owner` | string | No | Content owner ID on whose behalf the job is being created | | `report_type_id` | string | Yes | ID of the report type to generate (e.g., channel\_basic\_a2, channel\_demographics\_a1) | ## `youtube_reporting_jobs_delete` [Section titled “youtube\_reporting\_jobs\_delete”](#youtube_reporting_jobs_delete) Delete a scheduled YouTube Reporting API job. Stopping a job means new reports will no longer be generated. | Name | Type | Required | Description | | ---------------------------- | ------ | -------- | ---------------------------------------------------------- | | `job_id` | string | Yes | ID of the reporting job to delete | | `on_behalf_of_content_owner` | string | No | Content owner ID on whose behalf the request is being made | ## `youtube_reporting_list_jobs` [Section titled “youtube\_reporting\_list\_jobs”](#youtube_reporting_list_jobs) List all YouTube Reporting API jobs scheduled for a channel or content owner. | Name | Type | Required | Description | | ---------------------------- | ------- | -------- | -------------------------------------------------------------- | | `include_system_managed` | boolean | No | If true, include system-managed reporting jobs in the response | | `on_behalf_of_content_owner` | string | No | Content owner ID on whose behalf the request is being made | | `page_size` | integer | No | Maximum number of jobs to return per page | | `page_token` | string | No | Token for retrieving the next page of results | ## `youtube_reporting_list_report_types` [Section titled “youtube\_reporting\_list\_report\_types”](#youtube_reporting_list_report_types) List all YouTube Reporting API report types available for a channel or content owner (e.g., channel\_basic\_a2, channel\_demographics\_a1). | Name | Type | Required | Description | | ---------------------------- | ------- | -------- | ------------------------------------------------------------ | | `include_system_managed` | boolean | No | If true, include system-managed report types in the response | | `on_behalf_of_content_owner` | string | No | Content owner ID on whose behalf the request is being made | | `page_size` | integer | No | Maximum number of report types to return per page | | `page_token` | string | No | Token for retrieving the next page of results | ## `youtube_reporting_list_reports` [Section titled “youtube\_reporting\_list\_reports”](#youtube_reporting_list_reports) List reports that have been generated for a YouTube reporting job. Each report is a downloadable CSV file. | Name | Type | Required | Description | | ---------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------- | | `created_after` | string | No | Only return reports created after this timestamp (RFC3339 format, e.g., 2024-01-01T00:00:00Z) | | `job_id` | string | Yes | ID of the reporting job whose reports to list | | `on_behalf_of_content_owner` | string | No | Content owner ID on whose behalf the request is being made | | `page_size` | integer | No | Maximum number of reports to return per page | | `page_token` | string | No | Token for retrieving the next page of results | | `start_time_at_or_after` | string | No | Only return reports whose data start time is at or after this timestamp (RFC3339 format) | | `start_time_before` | string | No | Only return reports whose data start time is before this timestamp (RFC3339 format) | ## `youtube_search` [Section titled “youtube\_search”](#youtube_search) Search for videos, channels, and playlists on YouTube. Returns a list of resources matching the search query. The part parameter is fixed to ‘snippet’. Requires a valid YouTube OAuth2 connection. | Name | Type | Required | Description | | ------------------ | ------- | -------- | ------------------------------------------------------------------------ | | `channel_id` | string | No | Restrict search results to a specific channel | | `max_results` | integer | No | Maximum number of results to return (0-50, default: 10) | | `order` | string | No | Sort order for search results | | `page_token` | string | No | Token for pagination to retrieve the next page of results | | `published_after` | string | No | Filter results to resources published after this date (RFC 3339 format) | | `published_before` | string | No | Filter results to resources published before this date (RFC 3339 format) | | `q` | string | No | Search query keywords | | `safe_search` | string | No | Safe search filter level | | `type` | string | No | Restrict results to a specific resource type | | `video_duration` | string | No | Filter videos by duration (only applies when type is ‘video’) | ## `youtube_subscriptions_delete` [Section titled “youtube\_subscriptions\_delete”](#youtube_subscriptions_delete) Unsubscribe the authenticated user from a YouTube channel using the subscription ID. Requires youtube scope. | Name | Type | Required | Description | | ----------------- | ------ | -------- | -------------------------------- | | `subscription_id` | string | Yes | ID of the subscription to delete | ## `youtube_subscriptions_insert` [Section titled “youtube\_subscriptions\_insert”](#youtube_subscriptions_insert) Subscribe the authenticated user to a YouTube channel. Requires youtube scope. | Name | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------- | | `channel_id` | string | Yes | ID of the YouTube channel to subscribe to | ## `youtube_subscriptions_list` [Section titled “youtube\_subscriptions\_list”](#youtube_subscriptions_list) Retrieve a list of YouTube channel subscriptions for the authenticated user or a specific channel. You must provide exactly one filter: channel\_id, id, mine, my\_recent\_subscribers, or my\_subscribers. Requires a valid YouTube OAuth2 connection with youtube.readonly scope. | Name | Type | Required | Description | | ----------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `channel_id` | string | No | Return subscriptions for a specific channel. Use instead of id, mine, my\_recent\_subscribers, or my\_subscribers. | | `for_channel_id` | string | No | Filter subscriptions to specific channels (comma-separated channel IDs) | | `id` | string | No | Comma-separated list of subscription IDs to retrieve. Use instead of channel\_id, mine, my\_recent\_subscribers, or my\_subscribers. | | `max_results` | integer | No | Maximum number of subscriptions to return (0-50, default: 5) | | `mine` | boolean | No | Return subscriptions for the authenticated user. Use instead of channel\_id, id, my\_recent\_subscribers, or my\_subscribers. | | `my_recent_subscribers` | boolean | No | Return the authenticated user’s recent subscribers. Use instead of channel\_id, id, mine, or my\_subscribers. | | `my_subscribers` | boolean | No | Return the authenticated user’s subscribers. Use instead of channel\_id, id, mine, or my\_recent\_subscribers. | | `order` | string | No | Sort order for subscriptions | | `page_token` | string | No | Token for pagination | | `part` | string | Yes | Comma-separated list of subscription resource parts to include | ## `youtube_video_categories_list` [Section titled “youtube\_video\_categories\_list”](#youtube_video_categories_list) Retrieve a list of YouTube video categories available in a given region or by ID. You must provide exactly one filter: id or region\_code. The part parameter is fixed to ‘snippet’. Useful for setting the category when updating a video. Requires youtube.readonly scope. | Name | Type | Required | Description | | ------------- | ------ | -------- | --------------------------------------------------------------------------------------------------- | | `hl` | string | No | Language for the category names in the response (BCP-47) | | `id` | string | No | Comma-separated list of category IDs to retrieve. Use instead of region\_code. | | `region_code` | string | No | ISO 3166-1 alpha-2 country code to retrieve categories available in that region. Use instead of id. | ## `youtube_videos_delete` [Section titled “youtube\_videos\_delete”](#youtube_videos_delete) Permanently delete a YouTube video. This action cannot be undone. Requires youtube scope. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------- | | `video_id` | string | Yes | ID of the video to delete | ## `youtube_videos_get_rating` [Section titled “youtube\_videos\_get\_rating”](#youtube_videos_get_rating) Retrieve the authenticated user’s rating (like, dislike, or none) for one or more YouTube videos. The part parameter is fixed to ‘id’. Requires youtube.readonly scope. | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------------------------------------------------ | | `id` | string | Yes | Comma-separated list of YouTube video IDs to get ratings for | ## `youtube_videos_list` [Section titled “youtube\_videos\_list”](#youtube_videos_list) Retrieve detailed information about one or more YouTube videos including statistics, snippet, content details, and status. You must provide exactly one filter: id, chart, or my\_rating. Requires a valid YouTube OAuth2 connection. | Name | Type | Required | Description | | ------------------- | ------- | -------- | ------------------------------------------------------------------------------ | | `chart` | string | No | Retrieve a chart of the most popular videos. Use instead of id or my\_rating. | | `id` | string | No | Comma-separated list of YouTube video IDs. Use instead of chart or my\_rating. | | `max_results` | integer | No | Maximum number of results to return when using chart filter (1-50, default: 5) | | `my_rating` | string | No | Filter videos by the authenticated user’s rating. Use instead of id or chart. | | `page_token` | string | No | Token for pagination | | `part` | string | Yes | Comma-separated list of video resource parts to include in the response | | `region_code` | string | No | ISO 3166-1 alpha-2 country code to filter trending videos by region | | `video_category_id` | string | No | Filter most popular videos by category ID | ## `youtube_videos_rate` [Section titled “youtube\_videos\_rate”](#youtube_videos_rate) Like, dislike, or remove a rating from a YouTube video on behalf of the authenticated user. Requires youtube scope with youtube.force-ssl. | Name | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------- | | `rating` | string | Yes | Rating to apply to the video | | `video_id` | string | Yes | YouTube video ID to rate | ## `youtube_videos_update` [Section titled “youtube\_videos\_update”](#youtube_videos_update) Update metadata for an existing YouTube video. When updating snippet, both title and category\_id are required together. Requires youtube scope. | Name | Type | Required | Description | | ----------------------- | --------------- | -------- | ------------------------------------------------------------------------------ | | `category_id` | string | No | YouTube video category ID. Required together with title when updating snippet. | | `default_language` | string | No | Language of the video | | `description` | string | No | New video description | | `embeddable` | boolean | No | Whether the video can be embedded | | `license` | string | No | Video license | | `privacy_status` | string | No | New privacy setting | | `public_stats_viewable` | boolean | No | Whether stats are publicly visible | | `tags` | `array` | No | Video tags | | `title` | string | No | New video title. Required together with category\_id when updating snippet. | | `video_id` | string | Yes | ID of the video to update | --- # DOCUMENT BOUNDARY --- # Zendesk > Connect to Zendesk. Manage customer support tickets, users, organizations, and help desk operations Connect to Zendesk. Manage customer support tickets, users, organizations, and help desk operations ![Zendesk logo](https://cdn.scalekit.com/sk-connect/assets/provider-icons/zendesk.svg) Supports authentication: API KEY ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Zendesk API credentials with Scalekit so it can authenticate requests on your behalf. You’ll need your Zendesk subdomain, email address, and an API token from your Zendesk Admin Center. 1. ### Generate an API token * In your Zendesk Admin Center, go to **Apps and integrations** → **APIs** → **Zendesk API**. * Under **Settings**, enable **Token access**. ![Zendesk API configuration page with Allow API token access enabled](/.netlify/images?url=_astro%2Fenable-token-access.CHU4gF9M.png\&w=1728\&h=608\&dpl=69cce21a4f77360008b1503a) * Click **Add API token**, enter a description, and click **Create**. * Copy the token — it is only shown once. 2. ### Create a connection In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Zendesk** and click **Create**. 3. ### Create a connected account Go to **Connected Accounts** for your Zendesk connection and click **Add account**. Fill in the required fields: * **Your User’s ID** — a unique identifier for the user in your system * **Zendesk Domain** — your full Zendesk domain (e.g., `yourcompany.zendesk.com`) * **Email Address** — the Zendesk account email address * **API Token** — the token you copied in step 1 * Click **Save**. ![Add connected account form for Zendesk in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-connected-account.BOQ4BElf.png\&w=1518\&h=1208\&dpl=69cce21a4f77360008b1503a) ## Tool list [Section titled “Tool list”](#tool-list) ## `zendesk_groups_list` [Section titled “zendesk\_groups\_list”](#zendesk_groups_list) List all groups in Zendesk. Groups are used to organize agents and route tickets. | Name | Type | Required | Description | | ---------- | ------ | -------- | ----------------------------------- | | `page` | number | No | Page number for pagination | | `per_page` | number | No | Number of groups per page (max 100) | ## `zendesk_organization_get` [Section titled “zendesk\_organization\_get”](#zendesk_organization_get) Retrieve details of a specific Zendesk organization by ID. Returns organization name, domain names, tags, notes, shared ticket settings, and custom fields. | Name | Type | Required | Description | | ----------------- | ------ | -------- | ----------------------------------------------------------------------- | | `include` | string | No | Additional related data to include (e.g., lookup\_relationship\_fields) | | `organization_id` | number | Yes | The ID of the organization to retrieve | ## `zendesk_organizations_list` [Section titled “zendesk\_organizations\_list”](#zendesk_organizations_list) List all organizations in Zendesk with pagination support. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------------ | | `page` | number | No | Page number for pagination | | `per_page` | number | No | Number of organizations per page (max 100) | ## `zendesk_search_tickets` [Section titled “zendesk\_search\_tickets”](#zendesk_search_tickets) Search Zendesk tickets using a query string. Supports Zendesk’s search syntax (e.g., ‘type:ticket status:open’). Returns up to 1000 results with pagination. | Name | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------------------------------------------------- | | `page` | number | No | Page number for pagination | | `per_page` | number | No | Number of results per page (max 100) | | `query` | string | Yes | Search query string using Zendesk search syntax (e.g., ‘type:ticket status:open assignee:me’) | | `sort_by` | string | No | Field to sort results by (updated\_at, created\_at, priority, status, ticket\_type) | | `sort_order` | string | No | Sort direction: asc or desc (default: desc) | ## `zendesk_ticket_comments_list` [Section titled “zendesk\_ticket\_comments\_list”](#zendesk_ticket_comments_list) Retrieve all comments (public replies and internal notes) for a specific Zendesk ticket. Returns comment body, author, timestamps, and attachments. | Name | Type | Required | Description | | ----------------------- | ------- | -------- | ------------------------------------------------------------------- | | `include` | string | No | Sideloads to include. Accepts ‘users’ to list email CCs. | | `include_inline_images` | boolean | No | When true, inline images are listed as attachments (default: false) | | `sort_order` | string | No | Sort direction for comments: asc or desc (default: asc) | | `ticket_id` | number | Yes | The ID of the ticket whose comments to list | ## `zendesk_ticket_create` [Section titled “zendesk\_ticket\_create”](#zendesk_ticket_create) Create a new support ticket in Zendesk. Requires a comment/description and optionally a subject, priority, assignee, and tags. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ---------------------------------------------------------- | | `assignee_email` | string | No | Email of the agent to assign the ticket to | | `comment_body` | string | Yes | The description or first comment of the ticket | | `priority` | string | No | Ticket priority: urgent, high, normal, or low | | `status` | string | No | Ticket status: new, open, pending, hold, solved, or closed | | `subject` | string | No | The subject/title of the ticket | | `tags` | `array` | No | List of tags to apply to the ticket | | `type` | string | No | Ticket type: problem, incident, question, or task | ## `zendesk_ticket_get` [Section titled “zendesk\_ticket\_get”](#zendesk_ticket_get) Retrieve details of a specific Zendesk ticket by ID. Returns ticket properties including status, priority, subject, requester, assignee, and timestamps. | Name | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------------------------------- | | `include` | string | No | Comma-separated list of sideloads to include (e.g., users, groups, organizations) | | `ticket_id` | number | Yes | The ID of the ticket to retrieve | ## `zendesk_ticket_reply` [Section titled “zendesk\_ticket\_reply”](#zendesk_ticket_reply) Add a public reply or internal note to a Zendesk ticket. Set public to false for internal notes visible only to agents. | Name | Type | Required | Description | | ----------- | ------- | -------- | ----------------------------------------------------------------------------------- | | `body` | string | Yes | The reply message content (plain text, markdown supported) | | `public` | boolean | No | Whether the comment is public (true) or an internal note (false). Defaults to true. | | `ticket_id` | number | Yes | The ID of the ticket to reply to | ## `zendesk_ticket_update` [Section titled “zendesk\_ticket\_update”](#zendesk_ticket_update) Update an existing Zendesk ticket. Change status, priority, assignee, subject, tags, or any other writable ticket field. | Name | Type | Required | Description | | ---------------- | --------------- | -------- | ---------------------------------------------------------- | | `assignee_email` | string | No | Email of the agent to assign the ticket to | | `assignee_id` | number | No | ID of the agent to assign the ticket to | | `group_id` | number | No | ID of the group to assign the ticket to | | `priority` | string | No | Ticket priority: urgent, high, normal, or low | | `status` | string | No | Ticket status: new, open, pending, hold, solved, or closed | | `subject` | string | No | New subject/title for the ticket | | `tags` | `array` | No | List of tags to set on the ticket (replaces existing tags) | | `ticket_id` | number | Yes | The ID of the ticket to update | | `type` | string | No | Ticket type: problem, incident, question, or task | ## `zendesk_tickets_list` [Section titled “zendesk\_tickets\_list”](#zendesk_tickets_list) List tickets in Zendesk with sorting and pagination. Returns tickets for the authenticated agent’s account. | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------------------------------- | | `page` | number | No | Page number for pagination | | `per_page` | number | No | Number of tickets per page (max 100) | | `sort_by` | string | No | Field to sort by: created\_at, updated\_at, priority, status, ticket\_type | | `sort_order` | string | No | Sort direction: asc or desc (default: desc) | ## `zendesk_user_create` [Section titled “zendesk\_user\_create”](#zendesk_user_create) Create a new user in Zendesk. Can create end-users (customers), agents, or admins. Email is required for end-users. | Name | Type | Required | Description | | ----------------- | ------- | -------- | ----------------------------------------------------------- | | `email` | string | No | Primary email address of the user | | `name` | string | Yes | Full name of the user | | `organization_id` | number | No | ID of the organization to associate the user with | | `phone` | string | No | Primary phone number (E.164 format, e.g. +15551234567) | | `role` | string | No | User role: end-user, agent, or admin. Defaults to end-user. | | `verified` | boolean | No | Whether the user’s identity is verified. Defaults to false. | ## `zendesk_user_get` [Section titled “zendesk\_user\_get”](#zendesk_user_get) Retrieve details of a specific Zendesk user by ID. Returns user profile including name, email, role, organization, and account status. | Name | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------- | | `include` | string | No | Comma-separated list of sideloads to include | | `user_id` | number | Yes | The ID of the user to retrieve | ## `zendesk_users_list` [Section titled “zendesk\_users\_list”](#zendesk_users_list) List users in Zendesk. Filter by role (end-user, agent, admin) with pagination support. | Name | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------------------------------------ | | `page` | number | No | Page number for pagination | | `per_page` | number | No | Number of users per page (max 100) | | `role` | string | No | Filter by role: end-user, agent, or admin | | `sort` | string | No | Field to sort by. Prefix with - for descending (e.g. -created\_at) | ## `zendesk_views_list` [Section titled “zendesk\_views\_list”](#zendesk_views_list) List ticket views in Zendesk. Views are saved filters for organizing tickets by status, assignee, tags, and more. | Name | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------------------- | | `access` | string | No | Filter by access level: personal, shared, or account | | `page` | number | No | Page number for pagination | | `per_page` | number | No | Number of views per page (max 100) | | `sort_by` | string | No | Field to sort by: title, updated\_at, created\_at, or position | | `sort_order` | string | No | Sort direction: asc or desc | --- # DOCUMENT BOUNDARY --- # Zoom > Connect to Zoom. Schedule meetings, manage recordings, and handle video conferencing workflows Connect to Zoom. Schedule meetings, manage recordings, and handle video conferencing workflows ![Zoom logo](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/zoom.svg) Supports authentication: OAuth 2.0 ## Set up the agent connector [Section titled “Set up the agent connector”](#set-up-the-agent-connector) Register your Scalekit environment with the Zoom connector so Scalekit handles the authentication flow and token lifecycle for you. The connection name you create will be used to identify and invoke the connection programmatically. You’ll need your app credentials from the [Zoom App Marketplace](https://marketplace.zoom.us/). 1. ### Set up auth redirects * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Create Connection**. Find **Zoom** and click **Create**. Copy the redirect URI. It looks like `https:///sso/v1/oauth//callback`. ![Copy redirect URI from Scalekit dashboard](/.netlify/images?url=_astro%2Fuse-own-credentials-redirect-uri.DA3578jH.png\&w=960\&h=527\&dpl=69cce21a4f77360008b1503a) * In the [Zoom App Marketplace](https://marketplace.zoom.us/), open your app and go to **App Credentials**. * Paste the copied URI into the **Redirect URL for OAuth** field and also add it to the **OAuth allow list**. ![Add redirect URL in Zoom App Marketplace](/.netlify/images?url=_astro%2Fadd-redirect-uri.cINcpnZD.png\&w=1360\&h=784\&dpl=69cce21a4f77360008b1503a) 2. ### Get client credentials * In the [Zoom App Marketplace](https://marketplace.zoom.us/), open your app and go to **App Credentials**: * **Client ID** — listed under **Client ID** * **Client Secret** — listed under **Client Secret** 3. ### Add credentials in Scalekit * In [Scalekit dashboard](https://app.scalekit.com), go to **Agent Auth** → **Connections** and open the connection you created. * Enter your credentials: * Client ID (from your Zoom app) * Client Secret (from your Zoom app) * Permissions — select the scopes your app needs ![Add credentials in Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-credentials.CTcbuNaH.png\&w=1496\&h=390\&dpl=69cce21a4f77360008b1503a) * Click **Save**. ## Usage [Section titled “Usage”](#usage) Connect a user’s Zoom account and make API calls on their behalf — Scalekit handles OAuth and token management automatically. You can interact with Zoom in two ways — via direct proxy API calls or via Scalekit optimized tool calls. Scroll down to see the list of available Scalekit tools. ## Proxy API Calls * Node.js ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import 'dotenv/config'; 3 4 const connectionName = 'zoom'; // get your connection name from connection configurations 5 const identifier = 'user_123'; // your unique user identifier 6 7 // Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 8 const scalekit = new ScalekitClient( 9 process.env.SCALEKIT_ENV_URL, 10 process.env.SCALEKIT_CLIENT_ID, 11 process.env.SCALEKIT_CLIENT_SECRET 12 ); 13 const actions = scalekit.actions; 14 15 // Authenticate the user 16 const { link } = await actions.getAuthorizationLink({ 17 connectionName, 18 identifier, 19 }); 20 console.log('🔗 Authorize Zoom:', link); 21 process.stdout.write('Press Enter after authorizing...'); 22 await new Promise(r => process.stdin.once('data', r)); 23 24 // Make a request via Scalekit proxy 25 const result = await actions.request({ 26 connectionName, 27 identifier, 28 path: '/v2/users/me', 29 method: 'GET', 30 }); 31 console.log(result); ``` * Python ```python 1 import scalekit.client, os 2 from dotenv import load_dotenv 3 load_dotenv() 4 5 connection_name = "zoom" # get your connection name from connection configurations 6 identifier = "user_123" # your unique user identifier 7 8 # Get your credentials from app.scalekit.com → Developers → Settings → API Credentials 9 scalekit_client = 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_ENV_URL"), 13 ) 14 actions = scalekit_client.actions 15 16 # Authenticate the user 17 link_response = actions.get_authorization_link( 18 connection_name=connection_name, 19 identifier=identifier 20 ) 21 # present this link to your user for authorization, or click it yourself for testing 22 print("🔗 Authorize Zoom:", link_response.link) 23 input("Press Enter after authorizing...") 24 25 # Make a request via Scalekit proxy 26 result = actions.request( 27 connection_name=connection_name, 28 identifier=identifier, 29 path="/v2/users/me", 30 method="GET" 31 ) 32 print(result) ``` ## Scalekit Tools --- # DOCUMENT BOUNDARY --- # Glossary > A comprehensive glossary of terms related to authentication, authorization, and identity management in B2B SaaS applications. ## Access Token [Section titled “Access Token”](#access-token) * **Definition**: A credential (often a JWT) issued by the authorization server that the client uses to access the resource server. It represents the client’s authorization and typically has an expiry time and scopes attached. The resource server validates this token. ## Administrator [Section titled “Administrator”](#administrator) * **Definition**: An IT administrator responsible for managing identity provider configurations within a customer organization. ## Admin Portal [Section titled “Admin Portal”](#admin-portal) * **Definition**: A customizable web interface for customers’ IT administrators to manage identity provider configurations. ## AI Agent Identity and Attestation [Section titled “AI Agent Identity and Attestation”](#ai-agent-identity-and-attestation) * **Definition**: A process by which an AI agent proves its identity to an authorization server, often using cryptographic evidence (e.g. signed JWT assertions or hardware-backed keys), so the server can trust requests coming from that agent. ## API Endpoint [Section titled “API Endpoint”](#api-endpoint) * **Definition**: A specific URL where an API can be accessed to perform specific operations or retrieve data. ## API Key [Section titled “API Key”](#api-key) * **Definition**: A unique identifier used to authenticate API requests to Scalekit, allowing secure access to the platform’s features and services. ## App [Section titled “App”](#app) * **Definition**: Another term for an application, representing the software product or service sold to customers. ## Application [Section titled “Application”](#application) * **Definition**: The software product or service offered by B2B App developers to customers. * **Example**: A workspace can contain multiple applications. ## Audit Log [Section titled “Audit Log”](#audit-log) * **Definition**: A record of all activities and changes made within the B2B App, used for security and compliance purposes. ## Authentication [Section titled “Authentication”](#authentication) * **Definition**: The process of verifying the identity of a user or system attempting to access the B2B App. ## Authorization [Section titled “Authorization”](#authorization) * **Definition**: The process of determining what actions or resources a user is allowed to access within the B2B App. ## Authorization Server [Section titled “Authorization Server”](#authorization-server) * **Definition**: The server in OAuth that authenticates clients and issues tokens (could be a part of your SaaS or a third-party IdP like Okta Azure AD, etc.). It essentially says “Yes, client X, here is a token proving you are authenticated and allowed to do Y.” ## Authorization URL [Section titled “Authorization URL”](#authorization-url) * **Definition**: The URL to which users are redirected to grant authorization for the B2B App. ## B2B App [Section titled “B2B App”](#b2b-app) * **Definition**: An application designed for use by other businesses or organizations to streamline operations. ## B2B SaaS App [Section titled “B2B SaaS App”](#b2b-saas-app) * **Definition**: A type of B2B App delivered over the internet, allowing access without local installation. ## Claims [Section titled “Claims”](#claims) * **Definition**: Information about a user that is passed from an identity provider to a service provider during authentication. ## Client Credentials Flow [Section titled “Client Credentials Flow”](#client-credentials-flow) * **Definition**: The OAuth process where a machine client exchanges its client ID and secret for an access token from the auth server. No user involved. The resulting token represents the machine and carries scopes for what it can do. ## Configuration [Section titled “Configuration”](#configuration) * **Definition**: The settings and parameters that define how the B2B App interacts with Scalekit and other services. ## Connection [Section titled “Connection”](#connection) * **Definition**: A link between the B2B App and a customer’s identity provider for enabling Single Sign-On (SSO). * **Example**: Each organization can have its own unique connection. ## Customer [Section titled “Customer”](#customer) * **Definition**: A business or organization that uses the application to meet specific needs. ## Custom Attribute [Section titled “Custom Attribute”](#custom-attribute) * **Definition**: Additional fields added to user data in Scalekit for storing extra information. ## Dashboard [Section titled “Dashboard”](#dashboard) * **Definition**: The main control panel within Scalekit for configuring settings, viewing analytics, and managing integrations. ## Deprovisioning [Section titled “Deprovisioning”](#deprovisioning) * **Definition**: The process of removing user access and accounts when they are no longer needed or authorized. ## Directory Provider [Section titled “Directory Provider”](#directory-provider) * **Definition**: An organization offering directory services, including identity providers. ## Directory Sync [Section titled “Directory Sync”](#directory-sync) * **Definition**: A module in Scalekit for automatic provisioning and deprovisioning of user accounts. ## Documentation [Section titled “Documentation”](#documentation) * **Definition**: Comprehensive guides and references that explain how to use and integrate with Scalekit’s features and services. ## Dynamic Client Registration [Section titled “Dynamic Client Registration”](#dynamic-client-registration) * **Definition**: A protocol (RFC 7591) that allows a client application to programmatically register itself with an authorization server to obtain credentials (client ID/secret, etc.). Useful for large-scale or third-party ecosystems where manual registration of clients is not feasible or to enable self-service integration in a controlled way. ## Environment [Section titled “Environment”](#environment) * **Definition**: Different versions or instances of an application, such as test and live environments. * **Example**: Each environment has its own settings and is isolated for security. ## Error Handling [Section titled “Error Handling”](#error-handling) * **Definition**: The process of managing and responding to errors that occur during API calls or application operations. ## Federation [Section titled “Federation”](#federation) * **Definition**: The process of establishing trust between different identity providers and service providers for seamless authentication. ## ID Token [Section titled “ID Token”](#id-token) * **Definition**: A JSON Web Token (JWT) issued by the identity provider containing user identity information. ## Identity Provider (IdP) [Section titled “Identity Provider (IdP)”](#identity-provider-idp) * **Definition**: A service that verifies user identity and provides information about user attributes. ## IdP Simulator [Section titled “IdP Simulator”](#idp-simulator) * **Definition**: A tool that mimics the behavior of an identity provider for testing integrations. ## Integration [Section titled “Integration”](#integration) * **Definition**: The process of connecting Scalekit with other systems or services to enable seamless data flow and functionality. ## JWT [Section titled “JWT”](#jwt) * **Definition**: A standard format for representing claims securely between two parties. It is a compact, URL-safe means of representing claims securely between two parties. ## Logout [Section titled “Logout”](#logout) * **Definition**: The process of ending a user’s session and revoking their access to the B2B App. ## Machine-to-Machine (M2M) Authentication [Section titled “Machine-to-Machine (M2M) Authentication”](#machine-to-machine-m2m-authentication) * **Definition**: Methods for verifying identity between two automated services or software entities without human intervention. Ensures a client program (machine) is trusted by the service it calls, typically via tokens, keys, or certificates. ## MFA (Multi-Factor Authentication) [Section titled “MFA (Multi-Factor Authentication)”](#mfa-multi-factor-authentication) * **Definition**: A security feature that requires users to provide multiple forms of verification before accessing the B2B App. ## Model Context Protocol (MCP) [Section titled “Model Context Protocol (MCP)”](#model-context-protocol-mcp) * **Definition**: A new protocol (spearheaded by Anthropic and others) to standardize how AI models (assistants) can interact with external tools and data. It defines how AI agents can discover available “tools” (APIs) and the context to call them. For auth, MCP leverages OAuth 2.1 – effectively requiring AI agents to go through a secure authorization process to get access to those tools. Think of it as an evolving standard to make AI to SaaS integrations plug-and-play, with security built-in via OAuth. ## Mutual TLS (mTLS) [Section titled “Mutual TLS (mTLS)”](#mutual-tls-mtls) * **Definition**: A transport layer security mechanism where *both client and server present certificates* to mutually authenticate each other during the TLS handshake. Provides strong assurance of identities at connection level and encrypts the traffic. Used in high-security environments and internal service-to-service auth. ## Normalized Payload [Section titled “Normalized Payload”](#normalized-payload) * **Definition**: A standardized format for data sent from Scalekit to the B2B App. ## OAuth [Section titled “OAuth”](#oauth) * **Definition**: A standard protocol for authorization enabling limited access to user data. ## OAuth 2.0/OAuth 2.1 [Section titled “OAuth 2.0/OAuth 2.1”](#oauth-20oauth-21) * **Definition**: An authorization framework widely used for granting access to resources. OAuth 2.0 defines various *flows* (grant types) for different scenarios (authorization code, client credentials, etc.). OAuth 2.1 is an incremental update that compiles security best practices (PKCE required, no legacy flows, etc.). In M2M context, OAuth’s **Client Credentials Grant** is most relevant, allowing a service to get an access token using its own credentials. ## OAuth 2.0 Token Exchange (RFC 8693) [Section titled “OAuth 2.0 Token Exchange (RFC 8693)”](#oauth-20-token-exchange-rfc-8693) * **Definition**: A protocol that lets one token be exchanged for another—for example, an AI agent exchanging its machine-client token for a token scoped to call a downstream service on behalf of a user or another service. Enables delegation and impersonation scenarios. ## OIDC [Section titled “OIDC”](#oidc) * **Definition**: A standard protocol for authentication that builds on OAuth 2.0. ## OpenID Connect (OIDC) [Section titled “OpenID Connect (OIDC)”](#openid-connect-oidc) * **Definition**: An identity layer on top of OAuth 2.0 (often used for user authentication). Mentioned here because the discovery document and id\_token concepts come from OIDC. OIDC isn’t directly about M2M auth (it’s user-centric), but the OIDC discovery (`.well-known`) and JWT usage are leveraged in service auth too. ## Organization [Section titled “Organization”](#organization) * **Definition**: The customers of B2B Apps, typically businesses. * **Example**: Each business is considered an organization with its own users. ## PKCE (Proof Key for Code Exchange) [Section titled “PKCE (Proof Key for Code Exchange)”](#pkce-proof-key-for-code-exchange) * **Definition**: An extension to OAuth used to prevent interception of authorization codes. The client generates a random secret (code verifier) and sends a hashed version (code challenge) in the auth request, then must present the original secret when redeeming the code. Ensures that even if an attacker intercepts the auth code, they can’t exchange it without the secret. PKCE is now recommended for any OAuth client that can’t secure a client secret – including mobile, SPA, and some machine clients. ## PKI (Public Key Infrastructure) [Section titled “PKI (Public Key Infrastructure)”](#pki-public-key-infrastructure) * **Definition**: The system of certificate authorities, processes, and tools for managing digital certificates (like those used in mTLS). Involves issuing certs, distributing them, rotating when expired, revoking if compromised, etc. A robust PKI is needed to effectively use certificate-based auth at scale. ## Provisioning [Section titled “Provisioning”](#provisioning) * **Definition**: The process of creating and managing user accounts and access rights in the B2B App. ## Rate Limiting [Section titled “Rate Limiting”](#rate-limiting) * **Definition**: A mechanism that controls the rate of requests a user or application can make to the API within a specific time period. ## Refresh Token [Section titled “Refresh Token”](#refresh-token) * **Definition**: A long-lived token that can be used to get new access tokens without re-authenticating. In M2M auth, refresh tokens are rarely used because the client can just use its credentials again. Refresh tokens are more for user-based flows to avoid prompting the user frequently. ## Resource Server [Section titled “Resource Server”](#resource-server) * **Definition**: The API or service that the client wants to use – it receives tokens from clients and decides whether to accept them (by validating them). In our context, your SaaS API is a resource server that expects a valid token for requests. ## Role-Based Access Control (RBAC) [Section titled “Role-Based Access Control (RBAC)”](#role-based-access-control-rbac) * **Definition**: A method of regulating access to resources based on the roles of individual users within an organization. ## SAML Assertion [Section titled “SAML Assertion”](#saml-assertion) * **Definition**: A statement by an identity provider indicating a user’s authentication status. ## SCIM [Section titled “SCIM”](#scim) * **Definition**: SCIM (System for Cross-domain Identity Management) is a standard protocol for automating the provisioning and deprovisioning of user accounts and their attributes between an identity provider and a service provider. ## Scopes [Section titled “Scopes”](#scopes) * **Definition**: Strings that define what access is being requested or granted in an OAuth token. For example, `read:inventory` or `payments:create`. Scopes let the token carry permissions, enabling the resource server to allow or deny requests based on scope. Principle of least privilege is implemented by granting minimal scopes. ## Service Account [Section titled “Service Account”](#service-account) * **Definition**: A non-human account used by a software service. In context, it’s an identity set up for a machine to use. For example, a service account could be created for “Data Sync Service” in a customer’s tenant on your app. Service accounts have credentials (like client ID/secret or keys) to authenticate, and usually have roles or scopes assigned just like a user would. They enable organization-level or service-level tokens without tying to an actual person. ## Service Provider [Section titled “Service Provider”](#service-provider) * **Definition**: An entity offering a product or service to another organization or individual, especially in SSO contexts. ## Session [Section titled “Session”](#session) * **Definition**: A period of interaction between a user and the B2B App, typically starting with authentication and ending with logout. ## Social Connection [Section titled “Social Connection”](#social-connection) * **Definition**: Allows users to sign in using their social media accounts. ## SSO (Single Sign-On) [Section titled “SSO (Single Sign-On)”](#sso-single-sign-on) * **Definition**: An authentication method that allows users to access multiple applications with a single set of credentials. ## Team Member [Section titled “Team Member”](#team-member) * **Definition**: Individuals from the B2B App developer’s company who use Scalekit to manage applications. * **Roles**: Can include developers, product managers, or customer support staff. ## Tenant [Section titled “Tenant”](#tenant) * **Definition**: An isolated instance of the B2B App for a specific customer organization, with its own data and configurations. ## Token [Section titled “Token”](#token) * **Definition**: A piece of data that represents a user’s authentication status and permissions, used for accessing protected resources. ## User [Section titled “User”](#user) * **Definition**: An individual who uses the B2B App, typically belonging to a customer organization. ## User Attribute [Section titled “User Attribute”](#user-attribute) * **Definition**: Properties describing a user’s identity, used for authentication and access control. ## Webhook [Section titled “Webhook”](#webhook) * **Definition**: A mechanism for the B2B App to receive notifications or updates from Scalekit. ## Webhook Payload [Section titled “Webhook Payload”](#webhook-payload) * **Definition**: The data sent by Scalekit to the B2B App when a webhook is triggered, containing information about the event. ## Workspace [Section titled “Workspace”](#workspace) * **Definition**: A centralized hub for B2B App developers to manage applications and settings. * **Example**: Think of it as a command center for efficient application management. ## Zero Trust Security [Section titled “Zero Trust Security”](#zero-trust-security) * **Definition**: A security model where no user or device is inherently trusted, even if inside the network. Every access request must be authenticated, authorized, and continuously validated. For M2M, this means authenticating every service communication, minimizing implicit trust, and verifying identities at multiple layers (network & application). It often involves micro-segmentation and strict identity and access management for every machine identity. --- # DOCUMENT BOUNDARY --- # Interceptor triggers > The points in the authentication flow where Scalekit calls your interceptor endpoint ## `PRE_SIGNUP` [Section titled “PRE\_SIGNUP”](#pre_signup) Fires before a user creates a new organization. Use this to validate email domains, check against blocklists, or enforce custom signup policies. ### Request body from Scalekit [Section titled “Request body from Scalekit”](#request-body-from-scalekit) PRE\_SIGNUP — request body ```json 1 { 2 "display_name": "Validate email domain", 3 "trigger_point": "PRE_SIGNUP", 4 "interceptor_context": { 5 "environment_id": "env_92561807201272162", 6 "user_id": "usr_93418238346728951", // Present only if user exists in another organization 7 "user_email": "john.doe@acmecorp.com", // Email attempting to sign up 8 "connection_details": [ 9 { 10 "id": "conn_92561808744978132", 11 "type": "OAUTH", // OAUTH, SAML, OIDC, or PASSWORDLESS 12 "provider": "GOOGLE" // Identity provider used for authentication 13 } 14 ], 15 //Contains parameters from the /oauth/authorize request 16 "auth_request": { 17 "connection_id": "conn_81665025441299343", 18 "organization_id": "org_102953846317318346", 19 "domain": "foocorp.com", 20 "login_hint": "john.doe@example.com", 21 "state": "xsrPHl7k7ARgdhC6" 22 }, 23 "device_type": "Desktop", // Desktop, Mobile, Tablet, or Unknown 24 "ip_address": "203.0.113.24", // Client's IP address for geolocation or blocklist checks 25 "region": "IN", // Two-letter country code 26 "city": "Bengaluru", 27 "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...", 28 "triggered_at": "2025-10-09T09:48:02.875Z" // ISO 8601 timestamp 29 }, 30 "data": { 31 // User object present only when user already exists in another organization 32 "user": { 33 "id": "usr_93418238346728951", 34 "name": "John Doe", 35 "email": "john.doe@acmecorp.com", 36 "email_verified": true, 37 "created_at": "2025-10-06T11:06:49.120Z", 38 "updated_at": "2025-10-06T13:33:06.479Z", 39 "given_name": "John", 40 "family_name": "Doe", 41 "metadata": { 42 "type": "social_user" 43 }, 44 "memberships": [ // Existing organization memberships 45 { 46 "organization_id": "org_93418204671239864", 47 "status": "ACTIVE", 48 "roles": [ 49 "admin" 50 ], 51 "metadata": { 52 "cost": { 53 "category": "platform", 54 "region": "US" 55 }, 56 "department": "engineering" 57 }, 58 "organization_name": "Example inc" 59 } 60 ] 61 } 62 } 63 } ``` ### Response format to return [Section titled “Response format to return”](#response-format-to-return) PRE\_SIGNUP — response body ```json 1 { 2 // Required: choose ALLOW or DENY 3 "decision": "DENY", // ALLOW | DENY 4 // Optional with DENY 5 "error": { 6 "message": "Only @acmecorp.com email addresses are allowed to sign up" // Shown to user when DENY 7 }, 8 // Optional with ALLOW, Include when the user is to be provisioned in an existing organization. 9 "response": { 10 "create_organization_membership": { 11 // either external_organization_id or organization_id is required 12 "external_organization_id": "ext_B6YycAGRaPmnuxAFPT5KI4HBHxr4qWX", 13 "organization_id": "org_102953846317318346", 14 "roles": [ 15 "admin", 16 "viewer" 17 ] 18 } 19 } 20 } ``` ## `PRE_SESSION_CREATION` [Section titled “PRE\_SESSION\_CREATION”](#pre_session_creation) Fires before session tokens are issued for a user. Use this to add custom claims to tokens, apply conditional access policies, or integrate with external authorization systems. ### Request body from Scalekit [Section titled “Request body from Scalekit”](#request-body-from-scalekit-1) PRE\_SESSION\_CREATION — request body ```json 1 { 2 "display_name": "Add custom claims to tokens", 3 "trigger_point": "PRE_SESSION_CREATION", 4 "interceptor_context": { 5 "environment_id": "env_92561807204567213", 6 "user_id": "usr_93418238346728951", 7 "user_email": "john.doe@acmecorp.com", 8 "organization_id": "org_93418204671239864", // Organization user is logging into 9 "connection_details": [ 10 { 11 "id": "conn_92561808744978132", 12 "type": "OAUTH", // Authentication method used 13 "provider": "GOOGLE" 14 } 15 ], 16 "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...", 17 "device_type": "Desktop", // Desktop, Mobile, Tablet, or Unknown 18 "ip_address": "203.0.113.24", // Use for conditional access based on location 19 "region": "US", // Two-letter country code 20 "city": "San Francisco", 21 "triggered_at": "2025-10-08T15:22:42.381Z" // ISO 8601 timestamp 22 }, 23 "data": { 24 "user": { 25 "id": "usr_93418238346728951", 26 "name": "John Doe", 27 "email": "john.doe@acmecorp.com", 28 "email_verified": true, 29 "created_at": "2025-10-06T11:06:49.120Z", 30 "updated_at": "2025-10-06T13:33:06.479Z", 31 "first_name": "John", 32 "last_name": "Doe", 33 "memberships": [ // All organizations this user belongs to 34 { 35 "organization_id": "org_93418204671239864", 36 "status": "ACTIVE" 37 } 38 ] 39 } 40 } 41 } ``` ### Response format to return [Section titled “Response format to return”](#response-format-to-return-1) PRE\_SESSION\_CREATION — response body ```json 1 { 2 "decision": "ALLOW", // Required: ALLOW to issue tokens, DENY to block login 3 "response": { 4 "claims": { // Optional: Custom claims added to both access and ID tokens 5 "subscription_tier": "enterprise", 6 "data_region": "us-west-2", 7 "feature_flags": ["analytics_dashboard", "api_access", "custom_branding"], 8 "account_manager": "jane.smith@acmecorp.com" 9 } 10 } 11 } ``` Modify token claims in the response The `claims` field lets you add custom information that will be included in both access tokens and ID tokens issued by Scalekit. ## `PRE_USER_INVITATION` [Section titled “PRE\_USER\_INVITATION”](#pre_user_invitation) Fires before an invitation is created or sent for a new organization member. Use this to validate invitee email addresses, enforce invitation policies, or check user limits. ### Request body from Scalekit [Section titled “Request body from Scalekit”](#request-body-from-scalekit-2) PRE\_USER\_INVITATION — request body ```json 1 { 2 "display_name": "Validate invitation policy", 3 "trigger_point": "PRE_USER_INVITATION", 4 "interceptor_context": { 5 "environment_id": "env_92561807201272162", 6 "user_id": "usr_93418238346728951", // Present only if invitee already exists in another org 7 "user_email": "sarah.johnson@contractor.com", // Email address being invited 8 "organization_id": "org_93731871904672153", // Organization sending the invitation 9 "city": "Bengaluru", 10 "device_type": "Desktop", // Device of the person sending the invitation 11 "ip_address": "182.156.5.2", // IP of the person sending the invitation 12 "region": "IN", // Two-letter country code 13 "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...", 14 "triggered_at": "2025-10-09T12:50:41.803Z" // ISO 8601 timestamp 15 }, 16 "data": { 17 "organization": { // Organization details for context 18 "id": "org_93731871904672153", 19 "name": "Acme Corp" 20 } 21 } 22 } ``` ### Response format to return [Section titled “Response format to return”](#response-format-to-return-2) PRE\_USER\_INVITATION — response body ```json 1 { 2 "decision": "DENY", // Required: ALLOW to send invitation, DENY to block 3 "error": { 4 "message": "Cannot invite users from external domains. Please use @acmecorp.com email addresses." // Shown when DENY 5 } 6 } ``` ## `PRE_M2M_TOKEN_CREATION` [Section titled “PRE\_M2M\_TOKEN\_CREATION”](#pre_m2m_token_creation) Fires before issuing a machine-to-machine access token. Use this to add custom claims, modify scopes dynamically, or apply conditional access rules for service-to-service authentication. ### Request body from Scalekit [Section titled “Request body from Scalekit”](#request-body-from-scalekit-3) PRE\_M2M\_TOKEN\_CREATION — request body ```json 1 { 2 "display_name": "Validate M2M client permissions", 3 "trigger_point": "PRE_M2M_TOKEN_CREATION", 4 "interceptor_context": { 5 "environment_id": "env_17002334043308132", 6 "client_id": "m2morg_93710427703245914", // M2M client requesting the token 7 "user_agent": "deployment-service/2.1.0", // Service making the request 8 "device_type": "Unknown", 9 "triggered_at": "2025-10-08T21:22:20.173Z" // ISO 8601 timestamp 10 }, 11 "data": { 12 "m2m_token_claims": { // Claims that will be included in the token 13 "client_id": "m2morg_93710427703245914", 14 "claims": { 15 "custom_claims": { // Existing custom claims from client configuration 16 "service_name": "deployment-automation", 17 "deployment_environment": "production" 18 }, 19 "oid": "org_89669394174574792", // Organization ID for this M2M client 20 "scope": "deploy:applications read:deployments write:logs", // Space-separated scopes 21 "scopes": [ // Array of individual scopes requested 22 "deploy:applications", 23 "read:deployments", 24 "write:logs" 25 ] 26 } 27 } 28 } 29 } ``` ### Response format to return [Section titled “Response format to return”](#response-format-to-return-3) PRE\_M2M\_TOKEN\_CREATION — response body ```json 1 { 2 "decision": "ALLOW", // Required: ALLOW to issue token, DENY to block 3 "response": { 4 "claims": { // Optional: Add or modify claims in the M2M token 5 "scope": "deploy:applications read:deployments", // Can modify scopes dynamically 6 "aud": "https://api.acmecorp.com", // Target audience for the token 7 "rate_limit": "1000", // Custom claim for rate limiting 8 "environment": "production" // Custom claim for environment context 9 } 10 } 11 } ``` --- # DOCUMENT BOUNDARY --- # Directory events > Explore the webhook events related to directory operations in Scalekit, including user and group creation, updates, and deletions. ## Directory connection events [Section titled “Directory connection events”](#directory-connection-events) ### `organization.directory_enabled` [Section titled “organization.directory\_enabled”](#organizationdirectory_enabled) This webhook is triggered when a directory sync is enabled. The event type is `organization.directory_enabled` organization.directory\_enabled ```json 1 { 2 "environment_id": "env_27758032200925221", 3 "id": "evt_55136848686613000", 4 "object": "Directory", 5 "occurred_at": "2025-01-15T08:55:22.802860294Z", 6 "organization_id": "org_55135410258444802", 7 "spec_version": "1", 8 "type": "organization.directory_enabled", 9 "data": { 10 "directory_type": "SCIM", 11 "enabled": false, 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 | Unique identifier for the directory connection | | `directory_type` | string | The type of directory synchronization | | `enabled` | boolean | Indicates if the directory sync is enabled | | `environment_id` | string | Identifier for the environment | | `last_sync_at` | null | Timestamp of the last synchronization, null if not yet synced | | `organization_id` | string | Identifier for the organization | | `provider` | string | The provider of the directory | | `updated_at` | string | Timestamp of when the configuration was last updated | | `occurred_at` | string | Timestamp of when the event occurred | ### `organization.directory_disabled` [Section titled “organization.directory\_disabled”](#organizationdirectory_disabled) This webhook is triggered when a directory sync is disabled. The event type is `organization.directory_disabled` organization.directory\_disabled ```json 1 { 2 "spec_version": "1", 3 "id": "evt_53891640779079756", 4 "type": "organization.directory_disabled", 5 "occurred_at": "2025-01-06T18:45:21.057814Z", 6 "environment_id": "env_53814739859406915", 7 "organization_id": "org_53879494091473415", 8 "object": "Directory", 9 "data": { 10 "directory_type": "SCIM", 11 "enabled": false, 12 "id": "dir_53879621145330183", 13 "organization_id": "org_53879494091473415", 14 "provider": "OKTA", 15 "updated_at": "2025-01-06T18:45:21.04978184Z" 16 } 17 } ``` | Field | Type | Description | | ----------------- | ------- | -------------------------------------------------------------------------------- | | `directory_type` | string | Type of directory protocol used for synchronization | | `enabled` | boolean | Indicates whether the directory synchronization is currently enabled or disabled | | `id` | string | Unique identifier for the directory connection | | `last_sync_at` | string | Timestamp of the most recent directory synchronization | | `organization_id` | string | Unique identifier of the organization associated with this directory | | `provider` | string | Identity provider for the directory connection | | `status` | string | Current status of the directory synchronization process | | `updated_at` | string | Timestamp of the most recent update to the directory connection | | `occurred_at` | string | Timestamp of when the event occurred | ## Directory User Events [Section titled “Directory User Events”](#directory-user-events) ### `organization.directory.user_created` [Section titled “organization.directory.user\_created”](#organizationdirectoryuser_created) This webhook is triggered when a new directory user is created. The event type is `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 "cost_center": "QAUZJUHSTYCN", 12 "custom_attributes": { 13 "mobile_phone_number": "1-579-4072" 14 }, 15 "department": "HNXJPGISMIFN", 16 "division": "MJFUEYJOKICN", 17 "dp_id": "", 18 "email": "flavio@runolfsdottir.co.duk", 19 "employee_id": "AWNEDTILGaIZN", 20 "family_name": "Jaquelin", 21 "given_name": "Dayton", 22 "groups": [ 23 { 24 "id": "dirgroup_12312312312312", 25 "name": "Group Name" 26 } 27 ], 28 "id": "diruser_53891546960887884", 29 "language": "se", 30 "locale": "LLWLEWESPLDC", 31 "name": "QURGUZZDYMFU", 32 "nickname": "DTUODYKGFPPC", 33 "organization": "AUIITQVUQGVH", 34 "organization_id": "org_53879494091473415", 35 "phone_number": "1-579-4072", 36 "preferred_username": "kuntala1233a", 37 "profile": "YMIUQUHKGVAX", 38 "raw_attributes": {}, 39 "title": "FKQBHCWJXZSC", 40 "user_type": "RBQFJSQEFAEH", 41 "zoneinfo": "America/Araguaina", 42 "roles": [ 43 { 44 "role_name": "billing_admin" 45 } 46 ] 47 } 48 } ``` | Field | Type | Description | | -------------------- | ------- | ---------------------------------------------------------------------------------- | | `id` | string | Unique ID of the Directory User | | `organization_id` | string | Unique ID of the Organization to which this directory user belongs | | `dp_id` | string | Unique ID of the User in the Directory Provider (IdP) system | | `preferred_username` | string | Preferred username of the directory user | | `email` | string | Email of the directory user | | `active` | boolean | Indicates if the directory user is active | | `name` | string | Fully formatted name of the directory user | | `roles` | array | Array of roles assigned to the directory user | | `groups` | array | Array of groups to which the directory user belongs | | `given_name` | string | Given name of the directory user | | `family_name` | string | Family name of the directory user | | `nickname` | string | Nickname of the directory user | | `picture` | string | URL of the directory user’s profile picture | | `phone_number` | string | Phone number of the directory user | | `address` | object | Address of the directory user | | `custom_attributes` | object | Custom attributes of the directory user | | `raw_attributes` | object | Raw attributes of the directory user as received from the Directory Provider (IdP) | ### `organization.directory.user_updated` [Section titled “organization.directory.user\_updated”](#organizationdirectoryuser_updated) This webhook is triggered when a directory user is updated. The event type is `organization.directory.user_updated` organization.directory.user\_updated ```json 1 { 2 "spec_version": "1", 3 "id": "evt_53891546994442316", 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 "id": "diruser_12312312312312", 11 "organization_id": "org_53879494091473415", 12 "dp_id": "", 13 "preferred_username": "", 14 "email": "john.doe@example.com", 15 "active": true, 16 "name": "John Doe", 17 "roles": [ 18 { 19 "role_name": "billing_admin" 20 } 21 ], 22 "groups": [ 23 { 24 "id": "dirgroup_12312312312312", 25 "name": "Group Name" 26 } 27 ], 28 "given_name": "John", 29 "family_name": "Doe", 30 "nickname": "Jhonny boy", 31 "picture": "https://image.com/profile.jpg", 32 "phone_number": "1234567892", 33 "address": { 34 "postal_code": "64112", 35 "state": "Missouri", 36 "formatted": "123, Oxford Lane, Kansas City, Missouri, 64112" 37 }, 38 "custom_attributes": { 39 "attribute1": "value1", 40 "attribute2": "value2" 41 }, 42 "raw_attributes": {} 43 } 44 } ``` | Field | Type | Description | | -------------------- | ------- | ---------------------------------------------------------------------------------- | | `id` | string | Unique ID of the Directory User | | `organization_id` | string | Unique ID of the Organization to which this directory user belongs | | `dp_id` | string | Unique ID of the User in the Directory Provider (IdP) system | | `preferred_username` | string | Preferred username of the directory user | | `email` | string | Email of the directory user | | `active` | boolean | Indicates if the directory user is active | | `name` | string | Fully formatted name of the directory user | | `roles` | array | Array of roles assigned to the directory user | | `groups` | array | Array of groups to which the directory user belongs | | `given_name` | string | Given name of the directory user | | `family_name` | string | Family name of the directory user | | `nickname` | string | Nickname of the directory user | | `picture` | string | URL of the directory user’s profile picture | | `phone_number` | string | Phone number of the directory user | | `address` | object | Address of the directory user | | `custom_attributes` | object | Custom attributes of the directory user | | `raw_attributes` | object | Raw attributes of the directory user as received from the Directory Provider (IdP) | ### `organization.directory.user_deleted` [Section titled “organization.directory.user\_deleted”](#organizationdirectoryuser_deleted) This webhook is triggered when a directory user is deleted. The event type is `organization.directory.user_deleted` organization.directory.user\_deleted ```json 1 { 2 "spec_version": "1", 3 "id": "evt_53891546994442316", 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 "id": "diruser_12312312312312", 11 "organization_id": "org_12312312312312", 12 "dp_id": "", 13 "email": "john.doe@example.com" 14 } 15 } ``` | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------------------------ | | `id` | string | Unique ID of the Directory User | | `organization_id` | string | Unique ID of the Organization to which this directory user belongs | | `dp_id` | string | Unique ID of the User in the Directory Provider (IdP) system | | `email` | string | Email of the directory user | ## Directory Group Events [Section titled “Directory Group Events”](#directory-group-events) ### `organization.directory.group_created` [Section titled “organization.directory.group\_created”](#organizationdirectorygroup_created) This webhook is triggered when a new directory group is created. The event type is `organization.directory.group_created` organization.directory.group\_created ```json 1 { 2 "spec_version": "1", 3 "id": "evt_38862741515010639", 4 "environment_id": "env_32080745237316098", 5 "object": "DirectoryGroup", 6 "occurred_at": "2024-09-25T02:26:39.036398577Z", 7 "organization_id": "org_38609339635728478", 8 "type": "organization.directory.group_created", 9 "data": { 10 "directory_id": "dir_38610496391217780", 11 "display_name": "Avengers", 12 "external_id": null, 13 "id": "dirgroup_38862741498233423", 14 "organization_id": "org_38609339635728478", 15 "raw_attributes": {} 16 } 17 } ``` | Field | Type | Description | | ----------------- | ------ | --------------------------------------------------------- | | `directory_id` | string | Unique identifier for the directory | | `display_name` | string | Display name of the directory group | | `external_id` | null | External identifier for the group, null if not specified | | `id` | string | Unique identifier for the directory group | | `organization_id` | string | Identifier for the organization associated with the group | | `raw_attributes` | object | Raw attributes of the directory provider | ### `organization.directory.group_updated` [Section titled “organization.directory.group\_updated”](#organizationdirectorygroup_updated) This webhook is triggered when a directory group is updated. The event type is `organization.directory.group_updated` organization.directory.group\_updated ```json 1 { 2 "spec_version": "1", 3 "id": "evt_38864948910162368", 4 "organization_id": "org_38609339635728478", 5 "type": "organization.directory.group_updated", 6 "environment_id": "env_32080745237316098", 7 "object": "DirectoryGroup", 8 "occurred_at": "2024-09-25T02:48:34.745030921Z", 9 "data": { 10 "directory_id": "dir_38610496391217780", 11 "display_name": "Avengers", 12 "external_id": "", 13 "id": "dirgroup_38862741498233423", 14 "organization_id": "org_38609339635728478", 15 "raw_attributes": {} 16 } 17 } ``` | Field | Type | Description | | ----------------- | ------ | --------------------------------------------------------- | | `directory_id` | string | Unique identifier for the directory | | `display_name` | string | Display name of the directory group | | `external_id` | null | External identifier for the group, null if not specified | | `id` | string | Unique identifier for the directory group | | `organization_id` | string | Identifier for the organization associated with the group | | `raw_attributes` | object | Raw attributes of the directory group | ### `organization.directory.group_deleted` [Section titled “organization.directory.group\_deleted”](#organizationdirectorygroup_deleted) This webhook is triggered when a directory group is deleted. The event type is `organization.directory.group_deleted` organization.directory.group\_deleted ```json 1 { 2 "spec_version": "1", 3 "id": "evt_40650399597723966", 4 "environment_id": "env_12205603854221623", 5 "object": "DirectoryGroup", 6 "occurred_at": "2024-10-07T10:25:26.289331747Z", 7 "organization_id": "org_39802449573184223", 8 "type": "organization.directory.group_deleted", 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 } ``` | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------------------------- | | `directory_id` | string | Unique identifier for the directory | | `display_name` | string | Display name of the directory group | | `dp_id` | string | Unique identifier for the group in the directory provider system | | `id` | string | Unique identifier for the directory group | | `organization_id` | string | Identifier for the organization associated with the group | | `raw_attributes` | object | Raw attributes of the directory group as received from the provider | --- # DOCUMENT BOUNDARY --- # Organization events > Explore the webhook events related to organization operations in Scalekit, including creation, updates, and deletions. This page documents the webhook events related to organization operations in Scalekit. *** ## Organization events [Section titled “Organization events”](#organization-events) ### `organization.created` [Section titled “organization.created”](#organizationcreated) This webhook is triggered when a new organization is created. The event type is `organization.created` organization.created ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_1234567890", 4 "object": "Organization", 5 "occurred_at": "2024-01-15T10:30:00.123456789Z", 6 "organization_id": "org_1234567890", 7 "spec_version": "1", 8 "type": "organization.created", 9 "data": { 10 "create_time": "2025-12-09T09:25:02.02Z", 11 "display_name": "AcmeCorp", 12 "external_id": "org_external_123", 13 "id": "org_1234567890", 14 "metadata": null, 15 "region_code": "US", 16 "update_time": "2025-12-09T09:25:02.025330364Z", 17 "settings": { 18 "features": [ 19 { 20 "enabled": true, 21 "name": "sso" 22 }, 23 { 24 "enabled": false, 25 "name": "dir_sync" 26 } 27 ] 28 } 29 } 30 } ``` | Field | Type | Description | | ------------------- | -------------- | ----------------------------------------------------------------------------- | | `id` | string | Unique identifier for the organization | | `external_id` | string \| null | External identifier for the organization, if provided | | `display_name` | string \| null | Name of the organization, if provided | | `region_code` | string \| null | Geographic region code for the organization (US, EU), currently limited to US | | `create_time` | string | Timestamp of when the organization was created | | `update_time` | string \| null | Timestamp of when the organization was last updated | | `metadata` | object \| null | Additional metadata associated with the organization | | `settings` | object \| null | Organization settings including feature flags (sso, dir\_sync) | | `settings.features` | array | Array of feature objects with enabled status and name | ### `organization.updated` [Section titled “organization.updated”](#organizationupdated) This webhook is triggered when an organization is updated. The event type is `organization.updated` organization.updated ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_2345678901", 4 "object": "Organization", 5 "occurred_at": "2024-01-15T10:35:00.123456789Z", 6 "organization_id": "org_1234567890", 7 "spec_version": "1", 8 "type": "organization.updated", 9 "data": { 10 "create_time": "2025-12-09T09:25:02.02Z", 11 "display_name": "AcmeCorp", 12 "external_id": "org_external_123", 13 "id": "org_1234567890", 14 "metadata": null, 15 "region_code": "US", 16 "update_time": "2025-12-09T09:25:02.025330364Z", 17 "settings": { 18 "features": [ 19 { 20 "enabled": true, 21 "name": "sso" 22 }, 23 { 24 "enabled": false, 25 "name": "dir_sync" 26 } 27 ] 28 } 29 } 30 } ``` | Field | Type | Description | | ------------------- | -------------- | ----------------------------------------------------------------------------- | | `id` | string | Unique identifier for the organization | | `external_id` | string \| null | External identifier for the organization, if provided | | `display_name` | string \| null | Name of the organization, if provided | | `region_code` | string \| null | Geographic region code for the organization (US, EU), currently limited to US | | `create_time` | string | Timestamp of when the organization was created | | `update_time` | string \| null | Timestamp of when the organization was last updated | | `metadata` | object \| null | Additional metadata associated with the organization | | `settings` | object \| null | Organization settings including feature flags (sso, dir\_sync) | | `settings.features` | array | Array of feature objects with enabled status and name | ### `organization.deleted` [Section titled “organization.deleted”](#organizationdeleted) This webhook is triggered when an organization is deleted. The event type is `organization.deleted` organization.deleted ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_3456789012", 4 "object": "Organization", 5 "occurred_at": "2024-01-15T10:40:00.123456789Z", 6 "organization_id": "org_1234567890", 7 "spec_version": "1", 8 "type": "organization.deleted", 9 "data": { 10 "create_time": "2025-12-09T09:25:02.02Z", 11 "deleted_at": "2025-12-09T10:25:45.337417Z", 12 "display_name": "AcmeCorp", 13 "external_id": "org_external_123", 14 "id": "org_1234567890", 15 "metadata": null, 16 "region_code": "US", 17 "update_time": "2025-12-09T09:25:02.025330364Z", 18 "settings": { 10 collapsed lines 19 "features": [ 20 { 21 "enabled": true, 22 "name": "sso" 23 }, 24 { 25 "enabled": false, 26 "name": "dir_sync" 27 } 28 ] 29 } 30 } 31 } ``` | Field | Type | Description | | ------------------- | -------------- | ----------------------------------------------------------------------------- | | `id` | string | Unique identifier for the organization | | `external_id` | string \| null | External identifier for the organization, if provided | | `display_name` | string \| null | Name of the organization, if provided | | `region_code` | string \| null | Geographic region code for the organization (US, EU), currently limited to US | | `create_time` | string | Timestamp of when the organization was created | | `deleted_at` | string \| null | Timestamp of when the organization was deleted | | `update_time` | string \| null | Timestamp of when the organization was last updated | | `metadata` | object \| null | Additional metadata associated with the organization | | `settings` | object \| null | Organization settings including feature flags (sso, dir\_sync) | | `settings.features` | array | Array of feature objects with enabled status and name | --- # DOCUMENT BOUNDARY --- # Permission events > Explore the webhook events related to permission operations in Scalekit, including creation, updates, and deletions. This page documents the webhook events related to permission operations in Scalekit. *** ## Permission events [Section titled “Permission events”](#permission-events) ### `permission.created` [Section titled “permission.created”](#permissioncreated) This webhook is triggered when a new permission is created. The event type is `permission.created` permission.created ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_1234567890", 4 "object": "Permission", 5 "occurred_at": "2024-01-15T10:30:00.123456789Z", 6 "spec_version": "1", 7 "type": "permission.created", 8 "data": { 9 "description": "Permission to manage data", 10 "id": "perm_1234567890", 11 "name": "data:manage" 12 } 13 } ``` | Field | Type | Description | | ------------- | ------ | ----------------------------------------- | | `id` | string | Unique identifier for the permission | | `name` | string | Unique name identifier for the permission | | `description` | string | Description of what the permission allows | ### `permission.updated` [Section titled “permission.updated”](#permissionupdated) This webhook is triggered when a permission is updated. The event type is `permission.updated` permission.updated ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_2345678901", 4 "object": "Permission", 5 "occurred_at": "2024-01-15T10:35:00.123456789Z", 6 "spec_version": "1", 7 "type": "permission.updated", 8 "data": { 9 "description": "Updated permission to manage all data", 10 "id": "perm_1234567890", 11 "name": "data:manage" 12 } 13 } ``` | Field | Type | Description | | ------------- | ------ | ----------------------------------------- | | `id` | string | Unique identifier for the permission | | `name` | string | Unique name identifier for the permission | | `description` | string | Description of what the permission allows | ### `permission.deleted` [Section titled “permission.deleted”](#permissiondeleted) This webhook is triggered when a permission is deleted. The event type is `permission.deleted` permission.deleted ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_3456789012", 4 "object": "Permission", 5 "occurred_at": "2024-01-15T10:40:00.123456789Z", 6 "spec_version": "1", 7 "type": "permission.deleted", 8 "data": { 9 "description": "Updated permission to manage all data", 10 "id": "perm_1234567890", 11 "name": "data:manage" 12 } 13 } ``` | Field | Type | Description | | ------------- | ------ | ------------------------------------------------- | | `id` | string | Unique identifier for the deleted permission | | `name` | string | Unique name identifier for the deleted permission | | `description` | string | Description of what the permission allowed | --- # DOCUMENT BOUNDARY --- # Role events > Explore the webhook events related to role operations in Scalekit, including creation, updates, and deletions. This page documents the webhook events related to role operations in Scalekit. *** ## Role events [Section titled “Role events”](#role-events) ### `role.created` [Section titled “role.created”](#rolecreated) This webhook is triggered when a new role is created. The event type is `role.created` role.created ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_1234567890", 4 "object": "Role", 5 "occurred_at": "2024-01-15T10:30:00.123456789Z", 6 "spec_version": "1", 7 "type": "role.created", 8 "data": { 9 "description": "Viewer role with read-only access", 10 "display_name": "Viewer", 11 "extends": "member", 12 "id": "role_1234567890", 13 "name": "viewer" 14 } 15 } ``` | Field | Type | Description | | -------------- | ------ | -------------------------------------------- | | `id` | string | Unique identifier for the role | | `name` | string | Unique name identifier for the role | | `display_name` | string | Human-readable display name for the role | | `description` | string | Description of the role and its purpose | | `extends` | string | Name of the role that this role extends from | ### `role.updated` [Section titled “role.updated”](#roleupdated) This webhook is triggered when a role is updated. The event type is `role.updated` role.updated ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_2345678901", 4 "object": "Role", 5 "occurred_at": "2024-01-15T10:35:00.123456789Z", 6 "spec_version": "1", 7 "type": "role.updated", 8 "data": { 9 "description": "Updated viewer role with limited permissions", 10 "display_name": "Viewer", 11 "extends": "member", 12 "id": "role_1234567890", 13 "name": "viewer" 14 } 15 } ``` | Field | Type | Description | | -------------- | ------ | -------------------------------------------- | | `id` | string | Unique identifier for the role | | `name` | string | Unique name identifier for the role | | `display_name` | string | Human-readable display name for the role | | `description` | string | Description of the role and its purpose | | `extends` | string | Name of the role that this role extends from | ### `role.deleted` [Section titled “role.deleted”](#roledeleted) This webhook is triggered when a role is deleted. The event type is `role.deleted` role.deleted ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_3456789012", 4 "object": "Role", 5 "occurred_at": "2024-01-15T10:40:00.123456789Z", 6 "spec_version": "1", 7 "type": "role.deleted", 8 "data": { 9 "description": "Updated viewer role with limited permissions", 10 "display_name": "Viewer", 11 "extends": "member", 12 "id": "role_1234567890", 13 "name": "viewer" 14 } 15 } ``` | Field | Type | Description | | -------------- | ------ | ------------------------------------------------ | | `id` | string | Unique identifier for the deleted role | | `name` | string | Unique name identifier for the deleted role | | `display_name` | string | Human-readable display name for the deleted role | | `description` | string | Description of the role that was deleted | | `extends` | string | Name of the role that this role extends from | --- # DOCUMENT BOUNDARY --- # Enterprise SSO events > Explore the webhook events related to Enterprise SSO operations in Scalekit, including connection creation, enabling, disabling, and deletion. This page documents the webhook events related to Enterprise SSO connection operations in Scalekit. *** ## SSO connection events [Section titled “SSO connection events”](#sso-connection-events) ### `organization.sso_created` [Section titled “organization.sso\_created”](#organizationsso_created) This webhook is triggered when a new SSO connection is created for an organization. The event type is `organization.sso_created` organization.sso\_created ```json 1 { 2 "spec_version": "1", 3 "id": "evt_94567862441607493", 4 "object": "Connection", 5 "environment_id": "env_74418471961625391", 6 "occurred_at": "2025-10-14T09:27:18.488720586Z", 7 "organization_id": "org_83544995172188677", 8 "type": "organization.sso_created", 9 "data": { 10 "id": "conn_94567862424830277", 11 "organization_id": "org_83544995172188677", 12 "connection_type": "OIDC", 13 "provider": "OKTA" 14 } 15 } ``` | Field | Type | Description | | ----------------- | ------ | --------------------------------------------------------------- | | `id` | string | Unique identifier for the SSO connection | | `organization_id` | string | Identifier for the organization associated with this connection | | `connection_type` | string | Type of SSO connection (OIDC, SAML, etc.) | | `provider` | string | Identity provider for the SSO connection | ### `organization.sso_enabled` [Section titled “organization.sso\_enabled”](#organizationsso_enabled) This webhook is triggered when an SSO connection is enabled for an organization. The event type is `organization.sso_enabled` organization.sso\_enabled ```json 1 { 2 "spec_version": "1", 3 "id": "evt_94568078213382471", 4 "object": "Connection", 5 "environment_id": "env_74418471961625391", 6 "occurred_at": "2025-10-14T09:29:27.098914861Z", 7 "organization_id": "org_83544995172188677", 8 "type": "organization.sso_enabled", 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 } ``` | Field | Type | Description | | ----------------- | ------- | ------------------------------------------------------------------- | | `id` | string | Unique identifier for the SSO connection | | `organization_id` | string | Identifier for the organization associated with this connection | | `connection_type` | string | Type of SSO connection (OIDC, SAML, etc.) | | `provider` | string | Identity provider for the SSO connection | | `enabled` | boolean | Indicates whether the SSO connection is enabled (true in this case) | | `status` | string | Current status of the SSO connection configuration | ### `organization.sso_disabled` [Section titled “organization.sso\_disabled”](#organizationsso_disabled) This webhook is triggered when an SSO connection is disabled for an organization. The event type is `organization.sso_disabled` organization.sso\_disabled ```json 1 { 2 "spec_version": "1", 3 "id": "evt_94557976165089560", 4 "object": "Connection", 5 "environment_id": "env_74418471961625391", 6 "occurred_at": "2025-10-14T07:49:05.809554456Z", 7 "organization_id": "org_83544995172188677", 8 "type": "organization.sso_disabled", 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 } ``` | Field | Type | Description | | ----------------- | ------- | -------------------------------------------------------------------- | | `id` | string | Unique identifier for the SSO connection | | `organization_id` | string | Identifier for the organization associated with this connection | | `connection_type` | string | Type of SSO connection (OIDC, SAML, etc.) | | `provider` | string | Identity provider for the SSO connection | | `enabled` | boolean | Indicates whether the SSO connection is enabled (false in this case) | | `status` | string | Current status of the SSO connection configuration | ### `organization.sso_deleted` [Section titled “organization.sso\_deleted”](#organizationsso_deleted) This webhook is triggered when an SSO connection is deleted for an organization. The event type is `organization.sso_deleted` organization.sso\_deleted ```json 1 { 2 "spec_version": "1", 3 "id": "evt_94557997639926040", 4 "object": "Connection", 5 "environment_id": "env_74418471961625391", 6 "occurred_at": "2025-10-14T07:49:18.604546332Z", 7 "organization_id": "org_83544995172188677", 8 "type": "organization.sso_deleted", 9 "data": { 10 "id": "conn_83545002856153607", 11 "organization_id": "org_83544995172188677", 12 "connection_type": "OIDC", 13 "provider": "OKTA" 14 } 15 } ``` | Field | Type | Description | | ----------------- | ------ | --------------------------------------------------------------- | | `id` | string | Unique identifier for the SSO connection | | `organization_id` | string | Identifier for the organization associated with this connection | | `connection_type` | string | Type of SSO connection (OIDC, SAML, etc.) | | `provider` | string | Identity provider for the SSO connection | --- # DOCUMENT BOUNDARY --- # User events > Explore the webhook events related to user operations in Scalekit, including signup, login, logout, and organization membership events. This page documents the webhook events related to user operations in Scalekit. *** ## User authentication events [Section titled “User authentication events”](#user-authentication-events) ### `user.signup` [Section titled “user.signup”](#usersignup) This webhook is triggered when a user signs up to create a new organization. The event type is `user.signup`. user.signup ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_1234567890", 4 "object": "OrgMembershipEvent", 5 "occurred_at": "2024-01-15T10:30:00.123456789Z", 6 "spec_version": "1", 7 "type": "user.signup", 8 "data": { 9 "organization": { 10 "id": "org_1234567890", 11 "create_time": "2025-12-09T10:19:05.48Z", 12 "display_name": "", 13 "external_id": null, 14 "id": "org_102690563312124938", 15 "metadata": null, 16 "region_code": "US", 17 "update_time": "2025-12-09T12:04:41.386974738Z", 18 "settings": { 19 "features": [ 20 { 21 "enabled": true, 22 "name": "sso" 23 }, 24 { 25 "enabled": true, 26 "name": "dir_sync" 27 } 28 ] 29 } 30 }, 31 "user": { 32 "create_time": "2025-12-09T12:04:41.39Z", 33 "email": "amit.ash1996@gmail.com", 34 "external_id": "", 35 "id": "usr_102701193205121289", 36 "metadata": {}, 37 "update_time": "2025-12-09T12:04:41.391988278Z", 38 "user_profile": { 39 "custom_attributes": null, 40 "email_verified": true, 41 "external_identities": null, 42 "family_name": "doe", 43 "gender": "", 44 "given_name": "John", 45 "groups": null, 46 "id": "usp_102701193205186825", 47 "locale": "", 48 "metadata": {}, 49 "name": "John Doe", 50 "phone_number": "", 51 "phone_number_verified": false, 52 "picture": "https://lh3.googleusercontent.com/a/abcdef", 53 "preferred_username": "" 54 } 55 } 56 } 57 } ``` | Field | Type | Description | | -------------------------------- | -------------- | ----------------------------------------------------------------------------- | | `organization` | object | Details of organization that is created on signup | | `organization.id` | string | Unique identifier for the organization | | `organization.external_id` | string \| null | External identifier for the organization, if provided | | `organization.display_name` | string \| null | Name of the organization, if provided | | `organization.region_code` | string \| null | Geographic region code for the organization (US, EU), currently limited to US | | `organization.create_time` | string | Timestamp of when the organization was created | | `organization.update_time` | string \| null | Timestamp of when the organization was last updated | | `organization.metadata` | object \| null | Additional metadata associated with the organization | | `organization.settings` | object \| null | Organization settings including feature flags (sso, dir\_sync) | | `organization.settings.features` | array | Array of feature objects with enabled status and name | | `user` | object | User details for the signed-up user | | `user.id` | string | Unique identifier for the user | | `user.email` | string | Email address of the user | | `user.external_id` | string \| null | External identifier for the user, if provided | | `user.create_time` | string | Timestamp of when the user was created | | `user.update_time` | string | Timestamp of when the user was last updated | | `user.metadata` | string | Custom key-value pairs storing additional user context | | `user.user_profile` | object | User profile information | ### `user.login` [Section titled “user.login”](#userlogin) This webhook is triggered when a user logs in and a session is created. The event type is `user.login`. user.login ```json 1 { 2 "environment_id": "env_96736846679245078", 3 "id": "evt_102701193859432713", 4 "object": "UserLoginEvent", 5 "occurred_at": "2025-12-09T12:04:41.781873312Z", 6 "spec_version": "1", 7 "type": "user.login", 8 "data": { 9 "user": { 10 "create_time": "2025-12-09T12:04:41.39Z", 11 "email": "john.doe@acmecorp.com", 12 "external_id": "ext_123456789", 13 "id": "usr_123456789", 14 "last_login_time": "2025-12-09T12:04:41.48Z", 15 "metadata": {}, 16 "update_time": "2025-12-09T12:04:41.391988Z", 17 "user_profile": { 18 "custom_attributes": null, 19 "email_verified": true, 20 "external_identities": [ 21 { 22 "connection_id": "conn_97896332307464201", 23 "connection_provider": "GOOGLE", 24 "connection_type": "OAUTH", 25 "connection_user_id": "105055379523565727691", 26 "created_time": "2025-12-09T12:04:41.47Z", 27 "is_social": true, 28 "last_login_time": "2025-12-09T12:04:41.469311Z", 29 "last_synced_time": "2025-12-09T12:04:41.469311Z" 30 } 31 ], 32 "family_name": "Doe", 33 "gender": "", 34 "given_name": "John", 35 "groups": null, 36 "id": "usp_102701193205186825", 37 "locale": "", 38 "metadata": {}, 39 "name": "John Doe", 40 "phone_number": "", 41 "phone_number_verified": false, 42 "picture": "https://lh3.googleusercontent.com/a/abcdef", 43 "preferred_username": "" 44 } 45 }, 46 "user_session": { 47 "absolute_expires_at": "2026-01-08T12:04:41.737394Z", 48 "authenticated_organizations": ["org_102701193188409609"], 49 "created_at": "2025-12-09T12:04:41.48Z", 50 "expired_at": null, 51 "idle_expires_at": "2025-12-16T12:04:41.737395Z", 52 "last_active_at": "2025-12-09T12:04:41.747206Z", 53 "logout_at": null, 54 "organization_id": "org_102701193188409609", 55 "session_id": "ses_102701193356116233", 56 "status": "ACTIVE", 57 "updated_at": "2025-12-09T12:04:41.748512Z", 58 "user_id": "usr_102701193205121289", 59 "device": { 60 "browser": "Chrome", 61 "browser_version": "142.0.0.0", 62 "device_type": "Desktop", 63 "ip": "152.59.144.211", 64 "location": { 65 "city": "Patna", 66 "latitude": "25.594095", 67 "longitude": "85.137564", 68 "region": "IN", 69 "region_subdivision": "INBR" 70 }, 71 "os": "macOS", 72 "os_version": "10.15.7", 73 "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 74 } 75 } 76 } 77 } ``` | Field | Type | Description | | ------------------------------------------ | -------------- | ---------------------------------------------------------------------------------------------- | | `user` | object | User details for the logged-in user | | `user.id` | string | Unique identifier for the user | | `user.email` | string | Email address of the user | | `user.external_id` | string \| null | External identifier for the user, if provided | | `user.create_time` | string | Timestamp of when the user was created | | `user.update_time` | string | Timestamp of when the user was last updated | | `user.user_profile` | object | User profile information | | `user_session.absolute_expires_at` | string | Hard expiration timestamp for the session regardless of user activity | | `user_session.authenticated_organizations` | array | List of organization IDs that have been authenticated for this user within the current session | | `user_session.created_at` | string | Timestamp indicating when the session was created | | `user_session.expired_at` | string \| null | Timestamp when the session was terminated | | `user_session.idle_expires_at` | string | Projected expiration timestamp if the session remains idle without user activity | | `user_session.last_active_at` | string | Timestamp of the most recent user activity detected in this session | | `user_session.logout_at` | string \| null | Timestamp when the user explicitly logged out from the session | | `user_session.organization_id` | string | Organization ID for the user’s current active organization in this session | | `user_session.session_id` | string | Unique identifier for the session | | `user_session.status` | string | Current operational status of the session. Possible values: ‘active’ | | `user_session.updated_at` | string | Timestamp indicating when the session was last updated | | `user_session.user_id` | string | User ID for the user who owns this session | | `user_session.device` | object | Device metadata associated with this session | ### `user.logout` [Section titled “user.logout”](#userlogout) This webhook is triggered when a user’s session is terminated. The session termination could be due to user-initiated logout, idle or absolute session expiration, admin-administered session revocation. user.logout ```json 1 { 2 "environment_id": "env_96736846679245078", 3 "id": "evt_102708230123160586", 4 "object": "UserLogoutEvent", 5 "occurred_at": "2025-12-09T13:14:35.722070822Z", 6 "spec_version": "1", 7 "type": "user.logout", 8 "data": { 9 "user": { 10 "create_time": "2025-12-09T12:04:41.39Z", 11 "email": "john.doe@acmecorp.com", 12 "external_id": "ext_123456789", 13 "id": "usr_123456789", 14 "last_login_time": "2025-12-09T12:04:41.48Z", 15 "metadata": {}, 16 "update_time": "2025-12-09T12:04:41.391988Z", 17 "user_profile": { 18 "custom_attributes": null, 19 "email_verified": true, 20 "external_identities": [ 21 { 22 "connection_id": "conn_97896332307464201", 23 "connection_provider": "GOOGLE", 24 "connection_type": "OAUTH", 25 "connection_user_id": "105055379523565727691", 26 "created_time": "2025-12-09T12:04:41.47Z", 27 "is_social": true, 28 "last_login_time": "2025-12-09T12:04:41.469311Z", 29 "last_synced_time": "2025-12-09T12:04:41.469311Z" 30 } 31 ], 32 "family_name": "Charles", 33 "gender": "", 34 "given_name": "Dwayne", 35 "groups": null, 36 "id": "usp_102701193205186825", 37 "locale": "", 38 "metadata": {}, 39 "name": "Dwayne Charles", 40 "phone_number": "", 41 "phone_number_verified": false, 42 "picture": "https://lh3.googleusercontent.com/a/abcdef", 43 "preferred_username": "" 44 } 45 }, 46 "user_session": { 47 "absolute_expires_at": "2026-01-08T12:04:41.737394Z", 48 "authenticated_organizations": ["org_102701193188409609"], 49 "created_at": "2025-12-09T12:04:41.48Z", 50 "expired_at": null, 51 "idle_expires_at": "2025-12-16T12:04:41.737395Z", 52 "last_active_at": "2025-12-09T12:04:41.747206Z", 53 "logout_at": null, 54 "organization_id": "org_102701193188409609", 55 "session_id": "ses_102701193356116233", 56 "status": "ACTIVE", 57 "updated_at": "2025-12-09T12:04:41.748512Z", 58 "user_id": "usr_102701193205121289", 59 "device": { 60 "browser": "Chrome", 61 "browser_version": "142.0.0.0", 62 "device_type": "Desktop", 63 "ip": "152.59.144.211", 64 "location": { 65 "city": "Patna", 66 "latitude": "25.594095", 67 "longitude": "85.137564", 68 "region": "IN", 69 "region_subdivision": "INBR" 70 }, 71 "os": "macOS", 72 "os_version": "10.15.7", 73 "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 74 } 75 } 76 } 77 } ``` | Field | Type | Description | | ------------------------------------------ | -------------- | ---------------------------------------------------------------------------------------------- | | `user` | object | User details for the logged-in user | | `user.id` | string | Unique identifier for the user | | `user.email` | string | Email address of the user | | `user.external_id` | string \| null | External identifier for the user, if provided | | `user.create_time` | string | Timestamp of when the user was created | | `user.update_time` | string | Timestamp of when the user was last updated | | `user.user_profile` | object | User profile information | | `user_session.absolute_expires_at` | string | Hard expiration timestamp for the session regardless of user activity | | `user_session.authenticated_organizations` | array | List of organization IDs that have been authenticated for this user within the current session | | `user_session.created_at` | string | Timestamp indicating when the session was created | | `user_session.expired_at` | string \| null | Timestamp when the session was terminated | | `user_session.idle_expires_at` | string | Projected expiration timestamp if the session remains idle without user activity | | `user_session.last_active_at` | string | Timestamp of the most recent user activity detected in this session | | `user_session.logout_at` | string \| null | Timestamp when the user explicitly logged out from the session | | `user_session.organization_id` | string | Organization ID for the user’s current active organization in this session | | `user_session.session_id` | string | Unique identifier for the session | | `user_session.status` | string | Current operational status of the session. Possible values: ‘expired’, ‘revoked’, ‘logout’ | | `user_session.updated_at` | string | Timestamp indicating when the session was last updated | | `user_session.user_id` | string | User ID for the user who owns this session | | `user_session.device` | object | Device metadata associated with this session | ## Organization membership events [Section titled “Organization membership events”](#organization-membership-events) ### `user.organization_invitation` [Section titled “user.organization\_invitation”](#userorganization_invitation) This webhook is triggered when a user is invited to join an organization. The event type is `user.organization_invitation`. user.organization\_invitation ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_4567890123", 4 "object": "OrgMembershipEvent", 5 "occurred_at": "2024-01-15T11:00:00.123456789Z", 6 "spec_version": "1", 7 "type": "user.organization_invitation", 8 "data": { 9 "organization": { 10 "id": "org_1234567890", 11 "create_time": "2025-12-09T10:19:05.48Z", 12 "display_name": "Acme Corp", 13 "external_id": "org_external_123", 14 "id": "org_102690563312124938", 15 "metadata": null, 16 "region_code": "US", 17 "update_time": "2025-12-09T12:04:41.386974738Z", 18 "settings": { 19 "features": [ 20 { 21 "enabled": true, 22 "name": "sso" 23 }, 24 { 25 "enabled": true, 26 "name": "dir_sync" 27 } 28 ] 29 } 30 }, 31 "user": { 32 "create_time": "2025-12-09T12:04:41.39Z", 33 "email": "john.doe@acmecorp.com", 34 "external_id": "ext_123456789", 35 "id": "usr_123456789", 36 "metadata": {}, 37 "update_time": "2025-12-09T12:04:41.391988Z", 38 "user_profile": { 39 "custom_attributes": null, 40 "email_verified": true, 41 "external_identities": [ 42 { 43 "connection_id": "conn_97896332307464201", 44 "connection_provider": "GOOGLE", 45 "connection_type": "OAUTH", 46 "connection_user_id": "105055379523565727691", 47 "created_time": "2025-12-09T12:04:41.47Z", 48 "is_social": true, 49 "last_login_time": "2025-12-09T12:04:41.469311Z", 50 "last_synced_time": "2025-12-09T12:04:41.469311Z" 51 } 52 ], 53 "family_name": "Doe", 54 "gender": "", 55 "given_name": "John", 56 "groups": null, 57 "id": "usp_102701193205186825", 58 "locale": "", 59 "metadata": {}, 60 "name": "John Doe", 61 "phone_number": "", 62 "phone_number_verified": false, 63 "picture": "https://lh3.googleusercontent.com/a/abcdef", 64 "preferred_username": "" 65 } 66 } 67 } 68 } ``` | Field | Type | Description | | -------------------------------- | -------------- | ----------------------------------------------------------------------------- | | `organization` | object | Organization details for the invitation | | `organization.id` | string | Unique identifier for the organization | | `organization.external_id` | string \| null | External identifier for the organization if provided | | `organization.display_name` | string \| null | Name of the organization, if provided | | `organization.region_code` | string \| null | Geographic region code for the organization (US, EU), currently limited to US | | `organization.create_time` | string | Timestamp of when the organization was created | | `organization.update_time` | string \| null | Timestamp of when the organization was last updated | | `organization.metadata` | object \| null | Additional metadata associated with the organization | | `organization.settings` | object \| null | Organization settings including feature flags (sso, dir\_sync) | | `organization.settings.features` | array | Array of feature objects with enabled status and name | | `user` | object | User details for the invited user | | `user.id` | string | Unique identifier for the invited user | | `user.email` | string | Email address of the invited user | | `user.external_id` | string \| null | External identifier for the user, if provided | | `user.create_time` | string | Timestamp of when the user was created | | `user.update_time` | string | Timestamp of when the user was last updated | | `user.user_profile` | object | User profile information | ### `user.organization_membership_created` [Section titled “user.organization\_membership\_created”](#userorganization_membership_created) This webhook is triggered when a user joins an organization. The event type is `user.organization_membership_created`. user.organization\_membership\_created ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_5678901234", 4 "object": "OrgMembershipEvent", 5 "occurred_at": "2024-01-15T11:05:00.123456789Z", 6 "spec_version": "1", 7 "type": "user.organization_membership_created", 8 "data": { 9 "organization": { 10 "id": "org_1234567890", 11 "create_time": "2025-12-09T10:19:05.48Z", 12 "display_name": "Acme Corp", 13 "external_id": "org_external_123", 14 "id": "org_102690563312124938", 15 "metadata": null, 16 "region_code": "US", 17 "update_time": "2025-12-09T12:04:41.386974738Z", 18 "settings": { 19 "features": [ 20 { 21 "enabled": true, 22 "name": "sso" 23 }, 24 { 25 "enabled": true, 26 "name": "dir_sync" 27 } 28 ] 29 } 30 }, 31 "user": { 32 "create_time": "2025-12-09T12:04:41.39Z", 33 "email": "john.doe@acmecorp.com", 34 "external_id": "ext_123456789", 35 "id": "usr_123456789", 36 "metadata": {}, 37 "update_time": "2025-12-09T12:04:41.391988Z", 38 "user_profile": { 39 "custom_attributes": null, 40 "email_verified": true, 41 "external_identities": [ 42 { 43 "connection_id": "conn_97896332307464201", 44 "connection_provider": "GOOGLE", 45 "connection_type": "OAUTH", 46 "connection_user_id": "105055379523565727691", 47 "created_time": "2025-12-09T12:04:41.47Z", 48 "is_social": true, 49 "last_login_time": "2025-12-09T12:04:41.469311Z", 50 "last_synced_time": "2025-12-09T12:04:41.469311Z" 51 } 52 ], 53 "family_name": "Doe", 54 "gender": "", 55 "given_name": "John", 56 "groups": null, 57 "id": "usp_102701193205186825", 58 "locale": "", 59 "metadata": {}, 60 "name": "John Doe", 61 "phone_number": "", 62 "phone_number_verified": false, 63 "picture": "https://lh3.googleusercontent.com/a/abcdef", 64 "preferred_username": "" 65 } 66 } 67 } 68 } ``` | Field | Type | Description | | -------------------------------- | -------------- | ----------------------------------------------------------------------------- | | `organization` | object | Details of the organization which the user has joined | | `organization.id` | string | Unique identifier for the organization | | `organization.external_id` | string \| null | External identifier for the organization if provided | | `organization.display_name` | string \| null | Name of the organization, if provided | | `organization.region_code` | string \| null | Geographic region code for the organization (US, EU), currently limited to US | | `organization.create_time` | string | Timestamp of when the organization was created | | `organization.update_time` | string \| null | Timestamp of when the organization was last updated | | `organization.metadata` | object \| null | Additional metadata associated with the organization | | `organization.settings` | object \| null | Organization settings including feature flags (sso, dir\_sync) | | `organization.settings.features` | array | Array of feature objects with enabled status and name | | `user` | object | User details for the user who joined the organization | | `user.id` | string | Unique identifier for the user | | `user.email` | string | Email address of the user | | `user.external_id` | string \| null | External identifier for the user, if provided | | `user.create_time` | string | Timestamp of when the user was created | | `user.update_time` | string | Timestamp of when the user was last updated | | `user.user_profile` | object | User profile information | ### `user.organization_membership_updated` [Section titled “user.organization\_membership\_updated”](#userorganization_membership_updated) This webhook is triggered when a user’s organization membership is updated, e.g., change of user’s role in an organization. The event type is `user.organization_membership_updated`. user.organization\_membership\_updated ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_6789012345", 4 "object": "OrgMembershipEvent", 5 "occurred_at": "2024-01-15T11:10:00.123456789Z", 6 "spec_version": "1", 7 "type": "user.organization_membership_updated", 8 "data": { 9 "organization": { 10 "id": "org_1234567890", 11 "create_time": "2025-12-09T10:19:05.48Z", 12 "display_name": "Acme Corp", 13 "external_id": "org_external_123", 14 "id": "org_102690563312124938", 15 "metadata": null, 16 "region_code": "US", 17 "update_time": "2025-12-09T12:04:41.386974738Z", 18 "settings": { 19 "features": [ 20 { 21 "enabled": true, 22 "name": "sso" 23 }, 24 { 25 "enabled": true, 26 "name": "dir_sync" 27 } 28 ] 29 } 30 }, 31 "user": { 32 "create_time": "2025-12-09T12:04:41.39Z", 33 "email": "john.doe@acmecorp.com", 34 "external_id": "ext_123456789", 35 "id": "usr_123456789", 36 "metadata": {}, 37 "update_time": "2025-12-09T12:04:41.391988Z", 38 "user_profile": { 39 "custom_attributes": null, 40 "email_verified": true, 41 "external_identities": [ 42 { 43 "connection_id": "conn_97896332307464201", 44 "connection_provider": "GOOGLE", 45 "connection_type": "OAUTH", 46 "connection_user_id": "105055379523565727691", 47 "created_time": "2025-12-09T12:04:41.47Z", 48 "is_social": true, 49 "last_login_time": "2025-12-09T12:04:41.469311Z", 50 "last_synced_time": "2025-12-09T12:04:41.469311Z" 51 } 52 ], 53 "family_name": "Doe", 54 "gender": "", 55 "given_name": "John", 56 "groups": null, 57 "id": "usp_102701193205186825", 58 "locale": "", 59 "metadata": {}, 60 "name": "John Doe", 61 "phone_number": "", 62 "phone_number_verified": false, 63 "picture": "https://lh3.googleusercontent.com/a/abcdef", 64 "preferred_username": "" 65 } 66 } 67 } 68 } ``` | Field | Type | Description | | -------------------------------- | -------------- | --------------------------------------------------------------------------------- | | `organization` | object | Details of the organization for which users’ membership details have been updated | | `organization.id` | string | Unique identifier for the organization | | `organization.external_id` | string \| null | External identifier for the organization if provided | | `organization.display_name` | string \| null | Name of the organization, if provided | | `organization.region_code` | string \| null | Geographic region code for the organization (US, EU), currently limited to US | | `organization.create_time` | string | Timestamp of when the organization was created | | `organization.update_time` | string \| null | Timestamp of when the organization was last updated | | `organization.metadata` | object \| null | Additional metadata associated with the organization | | `organization.settings` | object \| null | Organization settings including feature flags (sso, dir\_sync) | | `organization.settings.features` | array | Array of feature objects with enabled status and name | | `user` | object | User details for the user whose organization membership has been updated | | `user.id` | string | Unique identifier for the user | | `user.email` | string | Email address of the user | | `user.external_id` | string \| null | External identifier for the user, if provided | | `user.create_time` | string | Timestamp of when the user was created | | `user.update_time` | string | Timestamp of when the user was last updated | | `user.user_profile` | object | User profile information | ### `user.organization_membership_deleted` [Section titled “user.organization\_membership\_deleted”](#userorganization_membership_deleted) This webhook is triggered when a user is removed from an organization. The event type is `user.organization_membership_deleted`. user.organization\_membership\_deleted ```json 1 { 2 "environment_id": "env_1234567890", 3 "id": "evt_7890123456", 4 "object": "OrgMembershipEvent", 5 "occurred_at": "2024-01-15T11:15:00.123456789Z", 6 "spec_version": "1", 7 "type": "user.organization_membership_deleted", 8 "data": { 9 "organization": { 10 "id": "org_1234567890", 11 "create_time": "2025-12-09T10:19:05.48Z", 12 "display_name": "Acme Corp", 13 "external_id": "org_external_123", 14 "id": "org_102690563312124938", 15 "metadata": null, 16 "region_code": "US", 17 "update_time": "2025-12-09T12:04:41.386974738Z", 18 "settings": { 19 "features": [ 20 { 21 "enabled": true, 22 "name": "sso" 23 }, 24 { 25 "enabled": true, 26 "name": "dir_sync" 27 } 28 ] 29 } 30 }, 31 "user": { 32 "create_time": "2025-12-09T12:04:41.39Z", 33 "email": "john.doe@acmecorp.com", 34 "external_id": "ext_123456789", 35 "id": "usr_123456789", 36 "metadata": {}, 37 "update_time": "2025-12-09T12:04:41.391988Z", 38 "user_profile": { 39 "custom_attributes": null, 40 "email_verified": true, 41 "external_identities": [ 42 { 43 "connection_id": "conn_97896332307464201", 44 "connection_provider": "GOOGLE", 45 "connection_type": "OAUTH", 46 "connection_user_id": "105055379523565727691", 47 "created_time": "2025-12-09T12:04:41.47Z", 48 "is_social": true, 49 "last_login_time": "2025-12-09T12:04:41.469311Z", 50 "last_synced_time": "2025-12-09T12:04:41.469311Z" 51 } 52 ], 53 "family_name": "Doe", 54 "gender": "", 55 "given_name": "John", 56 "groups": null, 57 "id": "usp_102701193205186825", 58 "locale": "", 59 "metadata": {}, 60 "name": "John Doe", 61 "phone_number": "", 62 "phone_number_verified": false, 63 "picture": "https://lh3.googleusercontent.com/a/abcdef", 64 "preferred_username": "" 65 } 66 } 67 } 68 } ``` | Field | Type | Description | | -------------------------------- | -------------- | ----------------------------------------------------------------------------- | | `organization` | object | Details of the organization from which the user has been removed | | `organization.id` | string | Unique identifier for the organization | | `organization.external_id` | string \| null | External identifier for the organization if provided | | `organization.display_name` | string \| null | Name of the organization, if provided | | `organization.region_code` | string \| null | Geographic region code for the organization (US, EU), currently limited to US | | `organization.create_time` | string | Timestamp of when the organization was created | | `organization.update_time` | string \| null | Timestamp of when the organization was last updated | | `organization.metadata` | object \| null | Additional metadata associated with the organization | | `organization.settings` | object \| null | Organization settings including feature flags (sso, dir\_sync) | | `organization.settings.features` | array | Array of feature objects with enabled status and name | | `user` | object | User details for the user who has been removed from an organization | | `user.id` | string | Unique identifier for the user | | `user.email` | string | Email address of the user | | `user.external_id` | string \| null | External identifier for the user, if provided | | `user.create_time` | string | Timestamp of when the user was created | | `user.update_time` | string | Timestamp of when the user was last updated | | `user.user_profile` | object | User profile information | --- # 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/) ### [Agent Auth](/resources/code-samples/agent-auth/) [Code samples for integrations with LangChain, Google ADK, and direct integrations](/resources/code-samples/agent-auth/) ### [Modular SSO](/resources/code-samples/modular-sso/) [Single Sign-On implementations for enterprise authentication with Express.js, .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 --- # Agent Auth > Code samples of AI agents using Scalekit along with LangChain, Google ADK, and direct integrations ### [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) --- # 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, .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 .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 --- # 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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` | | `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 * Use `login_hint` to improve user experience by prefilling login forms at the identity provider ## 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 organization_id="org_12345", 11 login_hint="user@example.com", 12 ) 13 14 +authorization_url = scalekit.get_authorization_url( 15 + redirect_uri, 16 + options 17 +) 18 # Example generated URL: 19 # 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/). Example scenario If you provide both `organization_id=org_123` and `login_hint=user@company.com`, Scalekit uses the organization’s SSO connection because `organization_id` has higher precedence than `login_hint` --- # 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 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 4 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 5 collapsed lines 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 6 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) 4 collapsed lines 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 8 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 != "" { 4 collapsed lines 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 8 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()); 4 collapsed lines 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) 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 The example below shows how to pass the Authorization URL to the Firebase Web SDK. * Firebase (Web SDK) Firebase Web SDK ```javascript 1 import { getAuth, OAuthProvider, signInWithRedirect } from "firebase/auth"; 2 3 // Security: Configure OIDC provider properly to prevent token injection 4 const auth = getAuth(); 5 6 // "scalekit" is the OIDC provider you configured in Firebase 7 const scalekitProvider = new OAuthProvider("scalekit"); 8 9 // Use the authorizationUrl generated in Step 3 10 scalekitProvider.setCustomParameters({ 11 connection_id: "", // Enables Firebase to forward the connection ID to Scalekit 12 }); 13 14 // Initiate Firebase sign-in with Scalekit provider 15 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=69cce21a4f77360008b1503a) 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.0.11" ``` ```xml com.scalekit scalekit-sdk-java 2.0.11 ``` ### 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 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 async function generatePortalLink(organizationId) { 10 const link = await scalekit.organization.generatePortalLink(organizationId); 11 return link.location; // Use as iframe src 12 } ``` * Python Flask ```python 6 collapsed lines 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 def generate_portal_link(organization_id): 11 link = scalekit_client.organization.generate_portal_link(organization_id) 12 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=69cce21a4f77360008b1503a) 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. **Verification methods:** * **DNS verification** Coming soon: Organization admins add a DNS TXT record to prove domain ownership through the admin portal * **Manual verification**: Request domain verification through the Scalekit dashboard when domain ownership is already established To manually verify a domain: * Navigate to **Dashboard > Organizations** and select the organization * Go to **Overview > Organization Domains** * Add the domain (e.g., `megacorp.com`) through the dashboard Once verified, users with email addresses from that domain (e.g., `user@megacorp.com`) can authenticate using their organization’s SSO. 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. ![Organization domain verification in dashboard](/.netlify/images?url=_astro%2Forg_domain.CnZ3T4x-.png\&w=2940\&h=1588\&dpl=69cce21a4f77360008b1503a) ## 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 --- # 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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. ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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 1 import type { IdTokenClaim } from '@scalekit-sdk/node'; 2 3 // Validate the ID token and cast to include your custom attributes 4 const claims = await scalekit.validateToken>(idToken); 5 const employeeNumber = claims['employee_number']; 6 const userRole = claims['user_role']; // use 'user_role', not 'roles' ``` * Python Read custom attributes from ID token ```python 1 # Validate the ID token — returns a dict of all claims 2 claims = scalekit_client.validate_token(id_token) 3 employee_number = claims.get('employee_number') 4 user_role = claims.get('user_role') # use 'user_role', not 'roles' ``` * Go Read custom attributes from ID token ```go 1 // Validate the ID token — returns a map of all claims 2 claims, err := scalekitClient.ValidateToken(ctx, idToken) 3 if err != nil { 4 log.Fatal(err) 5 } 6 employeeNumber := claims["employee_number"] 7 userRole := claims["user_role"] // use "user_role", not "roles" ``` * Java Read custom attributes from ID token ```java 1 import java.util.Map; 2 3 // Validate the ID token — returns a map of all claims 4 Map claims = scalekitClient.authentication().validateToken(idToken); 5 Object employeeNumber = claims.get("employee_number"); 6 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=69cce21a4f77360008b1503a) ### 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=69cce21a4f77360008b1503a) #### 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 1 // Use your test organization ID from the dashboard 2 const options = { 3 organizationId: 'org_32656XXXXXX0438' // Replace with your test organization ID 4 }; 5 6 // Generate Authorization URL that redirects to SSO Simulator 7 const authorizationURL = scalekit.getAuthorizationUrl(redirectUrl, options); 8 9 // Redirect user to start SSO flow 10 res.redirect(authorizationURL); ``` * Python Flask ```python 1 # Use your test organization ID from the dashboard 2 options = { 3 "organizationId": 'org_32656XXXXXX0438' # Replace with your test organization ID 4 } 5 6 # Generate Authorization URL that redirects to SSO Simulator 7 authorization_url = scalekit_client.get_authorization_url( 8 redirect_url, 9 options, 10 ) 11 12 # Redirect user to start SSO flow 13 return redirect(authorization_url) ``` * Go Gin ```go 1 // Use your test organization ID from the dashboard 2 options := scalekit.AuthorizationUrlOptions{ 3 OrganizationId: "org_32656XXXXXX0438", // Replace with your test organization ID 4 } 5 6 // Generate Authorization URL that redirects to SSO Simulator 7 authorizationURL := scalekitClient.GetAuthorizationUrl( 8 redirectUrl, 9 options, 10 ) 11 12 // Redirect user to start SSO flow 13 c.Redirect(http.StatusFound, authorizationURL) ``` * Java Spring Boot ```java 1 // Use your test organization ID from the dashboard 2 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 3 options.setOrganizationId("org_32656XXXXXX0438"); // Replace with your test organization ID 4 5 // Generate Authorization URL that redirects to SSO Simulator 6 String authorizationURL = scalekitClient 7 .authentication() 8 .getAuthorizationUrl(redirectUrl, options) 9 .toString(); 10 11 // Redirect user to start SSO flow 12 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) #### 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=69cce21a4f77360008b1503a) #### 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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](/guides/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=69cce21a4f77360008b1503a) #### 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=69cce21a4f77360008b1503a) #### 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](/guides/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 | An identifier for the user, as submitted by the identity provider that completed the single sign-on. | | `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 `sub` field is a concatenation of the `connection_id` and a unique identifier assigned to the user by the identity provider. * 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. --- # 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. ## 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 ``` | Incorrect SAML metadata configuration | Update SAML metadata URL with the correct value | | ``` 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 --- # Contact Us > Get in touch with the Scalekit team for support, schedule a call, or find answers to frequently asked questions about our services. If you encounter issues that remain unresolved despite your best troubleshooting efforts and our rigorous testing, please reach out to the Scalekit team using the contact information provided below. We will respond as quickly as possible. ### Talk to a dev [Write to us](mailto:support@scalekit.com) | [Schedule a call](https://schedule.scalekit.com/meet/ravi-madabhushi/demo-8b100203) ### Slack Community [Join our Slack community](https://join.slack.com/t/scalekit-community/shared_invite/zt-3gsxwr4hc-0tvhwT2b_qgVSIZQBQCWRw) to reach out for support and ask questions. --- # 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 3. Click on **Save** ![Save button](/.netlify/images?url=_astro%2F5.Omck9gZS.png\&w=1912\&h=1342\&dpl=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![Microsoft Entra ID](/.netlify/images?url=_astro%2F8.Cc6-NQ99.png\&w=1612\&h=932\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) Copy the **Redirect URl** from the **SSO Configuration Portal**. ![Copy Redirect URI](/.netlify/images?url=_astro%2F1.BcqKGAyd.png\&w=2206\&h=460\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) #### 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=69cce21a4f77360008b1503a) #### 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=69cce21a4f77360008b1503a) #### 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) *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=69cce21a4f77360008b1503a) *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=69cce21a4f77360008b1503a) *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=69cce21a4f77360008b1503a) *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=69cce21a4f77360008b1503a) *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 users or groups: 1. Go to the **User/group assignment** section in your identity provider application 2. Select and assign the user groups that need access to your application via SSO ![Group assignment](/.netlify/images?url=_astro%2F2.2-google-saml.BoETmMrW.png\&w=1874\&h=406\&dpl=69cce21a4f77360008b1503a) *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=69cce21a4f77360008b1503a) *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=69cce21a4f77360008b1503a) *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=69cce21a4f77360008b1503a) *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=69cce21a4f77360008b1503a) *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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 2. Click on **Add New Application** ![Click on Add New Application](/.netlify/images?url=_astro%2F2.CYy46Vv7.png\&w=2120\&h=896\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![Paste it under Default URL on JumpCloud Portal](/.netlify/images?url=_astro%2F8.BgrcZViX.png\&w=1736\&h=1014\&dpl=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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 ![](/.netlify/images?url=_astro%2F-1-1.DoY3Yfhj.png\&w=2558\&h=1172\&dpl=69cce21a4f77360008b1503a) Download Metadata XML file so that you can configure AD FS Server going forward ![](/.netlify/images?url=_astro%2F-1.BkbK6BJ4.png\&w=2260\&h=876\&dpl=69cce21a4f77360008b1503a) 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 ![](/.netlify/images?url=_astro%2F0-1.C1eDu6B8.png\&w=1262\&h=929\&dpl=69cce21a4f77360008b1503a) 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 ![](/.netlify/images?url=_astro%2F2.BzOVYbyq.png\&w=768\&h=634\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) The wizard will complete with the ‘Configure claims issuance policy for this application’ option automatically selected ![](/.netlify/images?url=_astro%2F6.4omJa0ZL.png\&w=768\&h=634\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) Click ‘Add Rule’ to create a new claim rule ![](/.netlify/images?url=_astro%2F7.CVY_QN4e.png\&w=538\&h=595\&dpl=69cce21a4f77360008b1503a) Select ‘Send LDAP Attributes as Claims’ template ![](/.netlify/images?url=_astro%2F8.CTl2bgd7.png\&w=768\&h=634\&dpl=69cce21a4f77360008b1503a) 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 ![](/.netlify/images?url=_astro%2F9.BslyN39j.png\&w=601\&h=642\&dpl=69cce21a4f77360008b1503a) 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 ![](/.netlify/images?url=_astro%2F12-1.CY8o-PyP.png\&w=2320\&h=1250\&dpl=69cce21a4f77360008b1503a) 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 ![](/.netlify/images?url=_astro%2F13.v5uvsTqZ.png\&w=2198\&h=978\&dpl=69cce21a4f77360008b1503a) 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 ![](/.netlify/images?url=_astro%2F14.BDS_w7Cj.png\&w=2194\&h=250\&dpl=69cce21a4f77360008b1503a) --- # 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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*. ![](/.netlify/images?url=_astro%2F0.BakodZRZ.png\&w=1542\&h=892\&dpl=69cce21a4f77360008b1503a) 2. In the **Applications** tab, click on **Create App Integration.** ![](/.netlify/images?url=_astro%2F1.IsoAY_Ly.png\&w=1406\&h=922\&dpl=69cce21a4f77360008b1503a) 3. Choose **SAML 2.0**, and click on **Next.** ![](/.netlify/images?url=_astro%2F2.DkynxeSj.png\&w=1840\&h=1108\&dpl=69cce21a4f77360008b1503a) 4. Give your app a name, choose your app visibility settings, and click on **Next.** ![](/.netlify/images?url=_astro%2F3.BB3z9eaj.png\&w=1368\&h=1084\&dpl=69cce21a4f77360008b1503a) ## 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**. ![](/.netlify/images?url=_astro%2F4.CHr3Qapy.png\&w=2292\&h=1116\&dpl=69cce21a4f77360008b1503a) ![](/.netlify/images?url=_astro%2F5.8eM-fLKR.png\&w=1894\&h=1398\&dpl=69cce21a4f77360008b1503a) 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.** ![](/.netlify/images?url=_astro%2F6.D0_xmfF5.png\&w=2292\&h=1116\&dpl=69cce21a4f77360008b1503a) ![](/.netlify/images?url=_astro%2F7.Dss7F_Tw.png\&w=1898\&h=1400\&dpl=69cce21a4f77360008b1503a) 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. ![](/.netlify/images?url=_astro%2F8.Duf235Yu.png\&w=1496\&h=696\&dpl=69cce21a4f77360008b1503a) ## Attribute Mapping [Section titled “Attribute Mapping”](#attribute-mapping) 1. Check the **Attribute Statements** section in the **SSO Configuration Portal**, and carefully map the same attributes on your Okta Admin Console. There are 2 ways that you may perform the mapping here. You may either use the Add expression buttons to add your attributes or through the legacy configurations. You will need to click on **Add expression** to add your required attributes. ![Attribute mapping on SSO Configuration Portal](/.netlify/images?url=_astro%2F20.B4Gf6htn.png\&w=1454\&h=730\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) With this, we 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) Click on **Add App.** ![Click on Add App](/.netlify/images?url=_astro%2F1.DJgsfl-m.png\&w=2586\&h=762\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) **OneLogin Admin Portal** ![](/.netlify/images?url=_astro%2F2025-12-18-14-28-46.BK5ps4c-.png\&w=2938\&h=1368\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![](/.netlify/images?url=_astro%2F7.H2z-QhcJ.png\&w=2890\&h=1276\&dpl=69cce21a4f77360008b1503a) Click on **Save**. ![Locate Save](/.netlify/images?url=_astro%2F8.uJ6aAmAa.png\&w=2582\&h=922\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) Click on **Save**. ![Save user assignment to application](/.netlify/images?url=_astro%2F14.DLRlndBF.png\&w=1244\&h=706\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) * Method 2: Import from URL ![Import from URL](/.netlify/images?url=_astro%2F1.2-ping-identity-saml.tLpFaw23.png\&w=720\&h=708\&dpl=69cce21a4f77360008b1503a) * Method 3: Manually Enter ![Manually Enter](/.netlify/images?url=_astro%2F1.3-ping-identity-saml.Cko2VJKF.png\&w=1592\&h=1568\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) #### 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) * 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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 1 /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 1 10 11 12 md:SPSSODescriptor 13 14 ``` 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 1 ``` 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 1 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 1 # Set to true to make encryption optional 2 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 1 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=69cce21a4f77360008b1503a) 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.BrW-0m3t.png\&w=2248\&h=1446\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) In the “Manage > All 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 3. ## Configure provisioning settings [Section titled “Configure provisioning settings”](#configure-provisioning-settings) 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.BMKcN9rF.png\&w=3458\&h=1464\&dpl=69cce21a4f77360008b1503a) 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.DakoZqn6.png\&w=3456\&h=1686\&dpl=69cce21a4f77360008b1503a) 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.B89WcEGp.png\&w=2364\&h=1858\&dpl=69cce21a4f77360008b1503a) ![Making sure the "Provision Microsoft Entra ID Users" is enabled.](/.netlify/images?url=_astro%2F08.rxOhmgro.png\&w=3442\&h=1634\&dpl=69cce21a4f77360008b1503a) 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.C1NOhTfh.png\&w=3438\&h=1488\&dpl=69cce21a4f77360008b1503a) 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. ## 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=69cce21a4f77360008b1503a) 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.Dpn2_u7L.png\&w=3916\&h=1390\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![SCIM Provisioning Setup](/.netlify/images?url=_astro%2F1-2-scimconfigs.Bw-b_DTR.png\&w=2010\&h=1466\&dpl=69cce21a4f77360008b1503a) 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.BG98eaHM.png\&w=3014\&h=1136\&dpl=69cce21a4f77360008b1503a) Create a custom application by trying to do an non-existent application search. ![Application Selection](/.netlify/images?url=_astro%2F3-custom-integration.C1znB9KY.png\&w=2972\&h=1684\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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.CdUuRJBI.png\&w=3000\&h=1700\&dpl=69cce21a4f77360008b1503a) 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.DnutNTQj.png\&w=3000\&h=1708\&dpl=69cce21a4f77360008b1503a) Click “Activate” and then “Save”. 5. ## Assign users and groups [Section titled “Assign users and groups”](#assign-users-and-groups) To assign users to the newly integrated application: ![User Assignment](/.netlify/images?url=_astro%2F8-group-assigned.DPZ894uA.png\&w=3000\&h=1702\&dpl=69cce21a4f77360008b1503a) 1. Go to “SSO Applications” and select the application you created. This opens an Modal. Select the User Group and click on “Save”. 2. Click on the “User Groups” tab and select the apps you want to assign to this group of users. 3. If you don’t have groups you can create one from “User Groups” tab. In this example, we have created a group called “YourApp Users” and assigned the “YourApp” app to it. 4. Click on “Save Group” to save the changes. 5. Now try adding a user to the group. If you don’t have users, you can create one from “Users” tab. 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ![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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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://help.okta.com/en-us/content/topics/users-groups-profiles/app-assignments-group-push.htm) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) --- # 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 Admin Portal from the app being onboarded and select the “SCIM Provisioning” tab. Choose “OneLogin” as your Directory Provider. ![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.DZxc5bYG.png\&w=1986\&h=1102\&dpl=69cce21a4f77360008b1503a) Click “Configure” to generate an Endpoint URL and Bearer token for your organization. ![OneLogin directory sync setup: Endpoint URL and one-time visible bearer token provided](/.netlify/images?url=_astro%2F0-2.CJeUb-77.png\&w=1944\&h=1378\&dpl=69cce21a4f77360008b1503a) Note If the “SCIM Provisioning” tab is not visible, contact the app owner to enable it for your organization. 2. ## Add a new application in OneLogin [Section titled “Add a new application in OneLogin”](#add-a-new-application-in-onelogin) In OneLogin, click “Administration” and then “Applications” from the top navigation pane. ![OneLogin Administration Applications](/.netlify/images?url=_astro%2F2.CIZ3WlOD.png\&w=3024\&h=1964\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) Search for “SCIM Provisioner with SAML (SCIM v2 Enterprise)” ![OneLogin application search results for "SCIM Provisioner with SAML" displaying SCIM v2 Enterprise option.](/.netlify/images?url=_astro%2F4.CGKi9joG.png\&w=3092\&h=812\&dpl=69cce21a4f77360008b1503a) Name the app (e.g., “Hero SaaS App”) and 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=69cce21a4f77360008b1503a) 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%2F6.PUUaw0Ru.png\&w=2574\&h=1260\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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, click ”+”, and assign “Hero SaaS App”. Click “Continue”. ![Assigning a new login to a user in OneLogin](/.netlify/images?url=_astro%2F8.Bd38Ai2c.png\&w=2998\&h=886\&dpl=69cce21a4f77360008b1503a) Click “Pending” to approve provisioning. ![OneLogin user provisioning dialog for creating Kitty Flake in Hero SaaS App, with options to approve or skip the action.](/.netlify/images?url=_astro%2F9.SCH24h8K.png\&w=2956\&h=944\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) This action informs the Hero SaaS app that the user is approved for access, and the app can create an account for them. You can see the new user added to the “Hero SaaS App” in the portal. ![OneLogin Directory Sync interface showing user details for Kitty Flake in Your organization, with SCIM integration status.](/.netlify/images?url=_astro%2F11.BNwpDRbV.png\&w=2286\&h=1126\&dpl=69cce21a4f77360008b1503a) 4. ## Configure group provisioning [Section titled “Configure group provisioning”](#configure-group-provisioning) Applications being onboarded may have roles that scope access, such as “admin” roles allowing users to perform administrator actions like deleting logs. You can choose which users in your organization get administrator access while others get member access. Note Labels such as “Member” and “Admin” are specific to the app. Check the “Access Roles” section in the configuration portal provided by the application (in this case, Hero SaaS App). The app owner must enable this section based on its applicability. To map users to groups in the app being onboarded: 1. Create a role in OneLogin. 2. Enable the inclusion of the Group parameter. 3. Create a rule that automatically picks up a user’s role value and sets it to the Group parameter. 4. Assign the role to the user. 5. Assign the user to the app. 5. ## Configure provisioning settings for groups [Section titled “Configure provisioning settings for groups”](#configure-provisioning-settings-for-groups) Navigate to the list of Applications and select “Hero SaaS App”. Ensure the provisioning workflow is enabled in the “Provisioning” tab. ![Setting up provisioning workflow for SCIM Provisioner with SAML in OneLogin, including options for user creation, deletion, and suspension actions.](/.netlify/images?url=_astro%2F6.PUUaw0Ru.png\&w=2574\&h=1260\&dpl=69cce21a4f77360008b1503a) Switch to “Rules” Tab and Click “Add Rule” button. Name the rule (e.g., “Assign Group in Hero SaaS”) and set the action to “Set Groups in Hero SaaS App” for each “role” with any value. ![Configuring a new mapping for group assignment in the Hero SaaS App using OneLogin.](/.netlify/images?url=_astro%2F13.BAmaVFFy.png\&w=2778\&h=1584\&dpl=69cce21a4f77360008b1503a) Select the “Parameters” tab, click on the “Groups” row, and check “Include in User Provisioning” in the popup. ![Configuring field groups in OneLogin for SCIM Provisioning, including SAML assertion and user provisioning options.](/.netlify/images?url=_astro%2F12.CEGCmpBj.png\&w=2916\&h=1264\&dpl=69cce21a4f77360008b1503a) 6. ## Create and assign roles [Section titled “Create and assign roles”](#create-and-assign-roles) Click on the user and navigate to “Applications”. ![Assigning the 'Hero SaaS App' to the 'hero\_saas\_viewer' role in OneLogin.](/.netlify/images?url=_astro%2F14.nhhVzkKj.png\&w=2932\&h=738\&dpl=69cce21a4f77360008b1503a) Add “Hero SaaS App” and select the roles to be passed to the app (treated as Groups by Hero SaaS App). ![Viewing application assignment and status for 'Test User' in OneLogin, with 'Hero SaaS App' pending.](/.netlify/images?url=_astro%2F16.BTKhJ96f.png\&w=2932\&h=920\&dpl=69cce21a4f77360008b1503a) Approve the user provisioning along with the Group. ![Approving 'Test User' for the 'Hero SaaS App' with assigned groups: Viewer and hero\_saas\_viewer.](/.netlify/images?url=_astro%2F17.DYeKWbDy.png\&w=2494\&h=822\&dpl=69cce21a4f77360008b1503a) Finally, verify that the groups are sent to the Hero SaaS App from the administrator portal where you configured the OneLogin connection. ![Directory sync status for Your Organization, showing linked groups and user count in OneLogin.](/.netlify/images?url=_astro%2F18.DdOVCwIt.png\&w=2328\&h=1426\&dpl=69cce21a4f77360008b1503a) --- # 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) In the rule configuration modal, set the following: * **Source**: Select **PingOne** * **Connection**: Choose the connection you created in the previous step * **Name**: Provide a meaningful name, such as the name of the application you are onboarding (e.g., “Hero SaaS”) Click **Save** to finalize the provisioning setup. ![Rule configuration with source, connection, and name](/.netlify/images?url=_astro%2Fsetup-rule.uLfcWCub.png\&w=3014\&h=2078\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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) --- # 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=69cce21a4f77360008b1503a) 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%2F1-navigate-to-social-logins.0QTBAQVD.png\&w=2622\&h=908\&dpl=69cce21a4f77360008b1503a) **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.DVSLNcJ6.png\&w=2554\&h=914\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) --- # 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=69cce21a4f77360008b1503a) 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%2F1-navigate-to-social-logins.0QTBAQVD.png\&w=2622\&h=908\&dpl=69cce21a4f77360008b1503a) ### 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.DVSLNcJ6.png\&w=2554\&h=914\&dpl=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) ### 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) --- # 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=69cce21a4f77360008b1503a) ### 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) * 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=69cce21a4f77360008b1503a) ### 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=69cce21a4f77360008b1503a) --- # 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=69cce21a4f77360008b1503a) 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%2F1-navigate-to-social-logins.0QTBAQVD.png\&w=2622\&h=908\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) --- # 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=69cce21a4f77360008b1503a) 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%2F1-navigate-to-social-logins.0QTBAQVD.png\&w=2622\&h=908\&dpl=69cce21a4f77360008b1503a) 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.DVSLNcJ6.png\&w=2554\&h=914\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) ## 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=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) 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%2F1-navigate-to-social-logins.0QTBAQVD.png\&w=2622\&h=908\&dpl=69cce21a4f77360008b1503a) ### 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.DVSLNcJ6.png\&w=2554\&h=914\&dpl=69cce21a4f77360008b1503a) 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=69cce21a4f77360008b1503a) ### 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=69cce21a4f77360008b1503a) --- # DOCUMENT BOUNDARY --- # Walkthrough implementing full stack authentication Watch the video walkthrough of implementing full stack authentication using Scalekit [Play](https://youtube.com/watch?v=Gnz8FYhHKI8) We’ll cover the following topics: * Setting up the Scalekit SDK * Implementing the login flow * Implementing the logout flow * Implementing the user management flow * Implementing the organization management flow --- # DOCUMENT BOUNDARY --- # Walkthrough implementing OAuth for MCP servers Watch the video walkthrough of implementing OAuth 2.1 authorization for MCP servers using Scalekit [Play](https://youtube.com/watch?v=-gFAWf5aSLw) We’ll cover the following topics: * Registering your MCP server * Implementing resource metadata discovery * Validating bearer tokens * Implementing scope-based authorization * Securing AI agent integrations --- # DOCUMENT BOUNDARY --- # Walkthrough implementing passwordless authentication Watch the video walkthrough of implementing passwordless authentication using Scalekit [Play](https://youtube.com/watch?v=8e4ZH-Aemg4) We’ll cover the following topics: * Configuring passwordless settings * Sending verification emails * Implementing OTP verification * Implementing magic link verification * Handling resend requests --- # DOCUMENT BOUNDARY --- # Walkthrough implementing SCIM provisioning Watch the video walkthrough of implementing SCIM user provisioning using Scalekit [Play](https://youtube.com/watch?v=SBJLtQaIbUk) We’ll cover the following topics: * Setting up directory connections * Using the Directory API to fetch users and groups * Implementing webhook endpoints for real-time provisioning * Handling user lifecycle events * Automating access management --- # DOCUMENT BOUNDARY --- # Walkthrough implementing enterprise SSO Watch the video walkthrough of implementing enterprise SSO using Scalekit [Play](https://youtube.com/watch?v=I7SZyFhKg-s) We’ll cover the following topics: * Setting up SSO connections * Configuring identity providers * Implementing authorization flows * Handling IdP-initiated SSO * Managing enterprise customer onboarding --- # DOCUMENT BOUNDARY --- # Agent framework integration examples > Examples showing how to integrate Scalekit Agent Auth with various AI frameworks including LangChain, Google ADK, and direct integrations. These examples demonstrate how to integrate Scalekit Agent Auth with AI frameworks for identity-aware tool calling and authenticated agent operations. * LangChain ## LangChain integration [Section titled “LangChain integration”](#langchain-integration) Agent Connect example integrating with LangChain for tool-calling workflows. This sample integrates Agent Connect with LangChain to perform identity-aware actions through tool calling. It illustrates auth setup and secure agent operations. [View repository ](https://github.com/scalekit-inc/sample-langchain-agent) * Google ADK ## Google ADK integration [Section titled “Google ADK integration”](#google-adk-integration) Example agent that connects Google ADK with Scalekit. This example shows how to integrate a Google ADK agent with Scalekit for authenticated operations and identity-aware workflows. [View repository ](https://github.com/scalekit-inc/google-adk-agent-example) * Direct integration ## Direct integration [Section titled “Direct integration”](#direct-integration) Direct Agent Auth integration examples in Python. This directory provides direct integration examples for Agent Auth using Python. It covers auth, tool definitions, and secured requests. [View repository ](https://github.com/scalekit-inc/python-connect-demos/tree/main/direct) --- # DOCUMENT BOUNDARY --- # AWS Cognito integration examples > Examples showing how to integrate AWS Cognito with Scalekit SSO using OpenID Connect (OIDC). These examples demonstrate how to integrate AWS Cognito with Scalekit using OpenID Connect (OIDC), covering provider setup, callback handling, token exchange, and session management. * Overview ## AWS Cognito integration [Section titled “AWS Cognito integration”](#aws-cognito-integration) AWS Cognito SSO integration using OpenID Connect. This repository demonstrates integrating AWS Cognito with Scalekit using OIDC. It covers provider setup, callback handling, and session management. [View repository ](https://github.com/scalekit-inc/scalekit-cognito-sso) * Next.js ## Cognito with Next.js [Section titled “Cognito with Next.js”](#cognito-with-nextjs) Next.js example integrating AWS Cognito with Scalekit over OIDC. This example connects a Next.js app to AWS Cognito through Scalekit using OIDC. It demonstrates redirects, token exchange, and secured pages. [View repository ](https://github.com/scalekit-inc/nextjs-example-apps/tree/main/cognito-scalekit) --- # DOCUMENT BOUNDARY --- # Firebase integration examples > Examples showing how to integrate Firebase Authentication with Scalekit SSO across different implementations. This example demonstrates how to integrate Firebase Authentication with Scalekit SSO, covering token exchange, session validation, and authentication handoff between systems. ## Firebase integration [Section titled “Firebase integration”](#firebase-integration) Firebase authentication integration with Scalekit SSO. This sample demonstrates how to integrate Firebase Authentication with Scalekit SSO, covering token exchange and session validation across systems. [View repository ](https://github.com/scalekit-inc/scalekit-firebase-sso) --- # DOCUMENT BOUNDARY --- # Exchange code for user profile > Learn how to exchange the authorization code for the user's profile *auth-code-exchange-scalekit-sdk.js* express.js ```typescript 1 import { Scalekit } from '@scalekit-sdk/node'; 2 3 const redirectUri = 'http://localhost:3000/api/callback'; 4 5 const scalekit = new Scalekit( 6 process.env.SCALEKIT_ENV_URL, 7 process.env.SCALEKIT_CLIENT_ID, 8 process.env.SCALEKIT_CLIENT_SECRET 9 ); 10 11 app.get('/api/callback', async (req, res) => { 12 const { error, error_description, code } = req.query; 13 14 if (error) { 15 console.error('SSO callback error:', error, error_description); 16 return; 17 } 18 19 try { 20 const { user, idToken } = await scalekit.authenticateWithCode( 21 code, 22 redirectUri 23 ); 24 25 // Continue with your application logged in experience 26 res.redirect('/profile'); 27 } catch (error) { 28 console.error('Token exchange error:', error); 29 } 30 }); ``` --- # DOCUMENT BOUNDARY --- # Client credentials auth with Scalekit API > Learn how to authenticate with the Scalekit API using client credentials *client-credentials-auth.ts* ```typescript 1 import axios from 'axios'; 2 3 /** 4 * Client Credentials OAuth 2.0 Flow 5 * This flow is used for server-to-server authentication where a client application 6 * authenticates itself (rather than a user) to access protected resources. 7 */ 8 9 // Configuration 10 const config = { 11 clientId: process.env.SCALEKIT_CLIENT_ID, 12 clientSecret: process.env.SCALEKIT_CLIENT_SECRET, 13 tokenUrl: `${process.env.SCALEKIT_ENVIRONMENT_URL}/oauth/token`, 14 scope: 'openid email profile', 15 }; 16 17 main(); 18 19 /** 20 * Get an access token using the client credentials flow 21 * @returns {Promise} The access token 22 */ 23 async function getClientCredentialsToken(): Promise { 24 try { 25 // Prepare the request body 26 const params = new URLSearchParams(); 27 params.append('grant_type', 'client_credentials'); 28 params.append('client_id', config.clientId); 29 params.append('client_secret', config.clientSecret); 30 31 if (config.scope) { 32 params.append('scope', config.scope); 33 } 34 35 // Make the token request 36 const response = await axios.post(config.tokenUrl, params, { 37 headers: { 38 'Content-Type': 'application/x-www-form-urlencoded', 39 }, 40 }); 41 42 // Extract and return the access token 43 const { access_token, expires_in } = response.data; 44 45 console.log( 46 `Token acquired successfully. Expires in ${expires_in} seconds.` 47 ); 48 49 return access_token; 50 } catch (error) { 51 console.error('Error getting client credentials token:', error); 52 throw new Error('Failed to obtain access token'); 53 } 54 } 55 56 /** 57 * Example usage: Make an authenticated API request 58 * @param {string} url - The API endpoint to call 59 * @returns {Promise} The API response 60 */ 61 async function makeAuthenticatedRequest(url: string): Promise { 62 try { 63 // Get the access token 64 const token = await getClientCredentialsToken(); 65 66 // Make the authenticated request 67 const response = await axios.get(url, { 68 headers: { 69 Authorization: `Bearer ${token}`, 70 }, 71 }); 72 73 return response.data; 74 } catch (error) { 75 console.error('Error making authenticated request:', error); 76 throw error; 77 } 78 } 79 80 // Example usage 81 async function main() { 82 try { 83 const data = await makeAuthenticatedRequest( 84 `${process.env.SCALEKIT_ENVIRONMENT_URL}/api/v1/organizations` 85 ); 86 console.log('API Response:', data); 87 } catch (error) { 88 console.error('Main function error:', error); 89 } 90 } ``` --- # DOCUMENT BOUNDARY --- # Admin portal embedding > Example showing embedded admin portal This example demonstrates embedding the Scalekit admin portal and securing access using the OAuth 2.0 client credentials flow for administrative operations. [View repository ](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/embed-admin-portal-sample) --- # DOCUMENT BOUNDARY --- # API collections > Postman and Bruno collections for testing Scalekit APIs. This repository contains Postman and Bruno collections for exploring and testing Scalekit APIs, useful for development and QA workflows. [View repository ](https://github.com/scalekit-inc/api-collections) --- # DOCUMENT BOUNDARY --- # Code gists collection > Curated gists with essential Scalekit code snippets. This repository aggregates helpful gists for building with the Scalekit API, including auth flows, token handling, and request examples. [View repository ](https://github.com/scalekit-inc/gists) --- # DOCUMENT BOUNDARY --- # Go SDK > Official Go SDK for OIDC and SAML SSO integration. The official Go SDK for integrating OIDC and SAML SSO with Scalekit. It provides utilities for token validation and secure service endpoints. [View repository ](https://github.com/scalekit-inc/scalekit-sdk-go) --- # DOCUMENT BOUNDARY --- # Java SDK > Official Java SDK with Spring Boot support for enterprise auth. The official Java SDK streamlines enterprise authentication, with Spring Boot integration patterns for secure login and session handling. [View repository ](https://github.com/scalekit-inc/scalekit-sdk-java) --- # DOCUMENT BOUNDARY --- # M2M code samples > Concise examples for machine-to-machine authentication. This collection provides essential snippets for machine-to-machine authentication, including token acquisition and secure API calls without user interaction. [View repository ](https://github.com/scalekit-inc/gists/tree/main/m2m) --- # DOCUMENT BOUNDARY --- # Browse Scalekit MCP auth demos > Model Context Protocol authentication examples and patterns. This repository contains MCP authentication demos showing how to authorize tools and calls using Scalekit, with examples and reusable patterns. [View repository ](https://github.com/scalekit-inc/mcp-auth-demos) --- # DOCUMENT BOUNDARY --- # Node.js SDK > Official Node.js SDK for OIDC and SAML integrations. The official Node.js SDK for integrating Scalekit with Node.js applications. It supports OIDC and SAML SSO and includes helpers for common auth tasks. [View repository ](https://github.com/scalekit-inc/scalekit-sdk-node) --- # DOCUMENT BOUNDARY --- # Python SDK > Official Python SDK with integrations for popular frameworks. The official Python SDK provides helpers and integrations for FastAPI, Django, and Flask to implement SSO and secure sessions with Scalekit. [View repository ](https://github.com/scalekit-inc/scalekit-sdk-python) --- # DOCUMENT BOUNDARY --- # SSO migrations example > Express.js example for migrating users between SSO providers. This example shows strategies for SSO migrations, including preserving sessions, mapping identities, and validating tokens during cutover. [View repository ](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/sso-migrations-express-example) --- # DOCUMENT BOUNDARY --- # Webhook events > Next.js example handling webhook events from Scalekit. This sample shows how to receive and validate webhook events in a Next.js app, including signature verification and event handling patterns. [View repository ](https://github.com/scalekit-inc/nextjs-example-apps/tree/main/webhook-events) --- # DOCUMENT BOUNDARY --- # Hosted login examples > Examples showing how to integrate Scalekit's hosted login experience into your Express.js applications. These examples demonstrate how to integrate Scalekit’s hosted login box into your applications. The hosted login provides a streamlined authentication experience while maintaining secure session management. * Express.js login box ## Express.js login box [Section titled “Express.js login box”](#expressjs-login-box) Express.js example integrating the Scalekit hosted login box. This sample integrates the hosted login box into an Express.js app, handling redirects, callbacks, and secure session cookies for protected routes. [View repository ](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/expressjs-loginbox-authn) * Managed login demo ## Managed login box demo [Section titled “Managed login box demo”](#managed-login-box-demo) Express.js demo using the Scalekit hosted login experience. This demo uses Scalekit’s hosted login box for a streamlined authentication flow, reducing client-side complexity while keeping sessions secure. [View repository ](https://github.com/scalekit-developers/managed-loginbox-expressjs-demo) --- # DOCUMENT BOUNDARY --- # SSO implementation examples > Examples demonstrating Single Sign-On implementations across different frameworks including Express.js, .NET Core, and Python. These examples demonstrate how to implement enterprise Single Sign-On (SSO) with Scalekit across different frameworks and protocols including OIDC, SAML, and SCIM. * Express.js ## Express.js SSO demo [Section titled “Express.js SSO demo”](#expressjs-sso-demo) Express.js demo showing Single Sign-On flows with Scalekit. This demo implements SSO with Express.js, covering OIDC login, callback handling, and session validation. Use it to learn how to add enterprise SSO to a Node.js app. [View repository ](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/sso-express-example) * .NET Core ## .NET Core examples [Section titled “.NET Core examples”](#net-core-examples) .NET Core samples for SAML and OIDC Single Sign-On. This repository provides .NET Core examples to integrate SAML and OIDC SSO with Scalekit. Use it to learn provider configuration and middleware patterns. [View repository ](https://github.com/scalekit-inc/dotnet-example-apps) * Python ## OIDC, SAML and SCIM examples [Section titled “OIDC, SAML and SCIM examples”](#oidc-saml-and-scim-examples) Python examples for OIDC, SAML, and SCIM with common providers. This repository contains Python examples for integrating with identity providers using OIDC, SAML, and SCIM. Explore patterns for login, provisioning, and user sync. [View repository ](https://github.com/scalekit-developers/oidc-saml-scim-examples)