optionalTool arguments matching the tool input schema
connectedAccountIdstringoptionalConnected account ID (ca\_...) when you already know it
connectorstringoptionalConnector slug when the tool name exists on more than one connector
organizationIdstringoptionalOrganization tenant ID when your app scopes auth and accounts by org
userIdstringoptionalYour application user ID when you map Scalekit accounts to internal users
Returns ExecuteToolResponse. Same shape as `scalekit.actions.executeTool`.
***
## Error handling
[Section titled “Error handling”](#error-handling)
```ts
1
import {
2
ScalekitNotFoundException,
3
ScalekitServerException,
4
} from '@scalekit-sdk/node';
5
6
try {
7
const account = await scalekit.actions.getConnectedAccount({
8
connectionName: 'gmail',
9
identifier: 'user@example.com',
10
});
11
} catch (err) {
12
if (err instanceof ScalekitNotFoundException) {
13
// Account does not exist: create it or redirect to auth
14
} else if (err instanceof ScalekitServerException) {
15
// Network or server error
16
console.error(err);
17
}
18
}
```
| Exception | When raised |
| ------------------------------- | -------------------------------- |
| `ScalekitNotFoundException` | Resource not found |
| `ScalekitUnauthorizedException` | Invalid credentials |
| `ScalekitForbiddenException` | Insufficient permissions |
| `ScalekitServerException` | Base class for all server errors |
---
# DOCUMENT BOUNDARY
---
# Python SDK reference
> Complete API reference for the Scalekit Python SDK: actions client, MCP server provisioning, framework adapters, tools client, and modifiers.
`scalekit_client.actions` is the primary interface for AgentKit. It handles connected account management, MCP server provisioning, tool execution, and framework integrations.
## Install and initialize
[Section titled “Install and initialize”](#install-and-initialize)
```bash
1
pip install scalekit-sdk-python
```
```python
1
import os
2
import scalekit.client
3
4
scalekit_client = scalekit.client.ScalekitClient(
5
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
6
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
7
env_url=os.getenv("SCALEKIT_ENV_URL"),
8
)
9
10
actions = scalekit_client.actions
```
***
## Actions client
[Section titled “Actions client”](#actions-client)
### Authentication
[Section titled “Authentication”](#authentication)
#### get\_authorization\_link
[Section titled “get\_authorization\_link”](#get_authorization_link)
Generates a time-limited OAuth magic link to authorize a user’s connection.
Input schema
NameTypeRequiredDescription
identifierstroptionalUser identifier (e.g. email)
connection\_namestroptionalConnector slug (e.g. gmail)
connected\_account\_idstroptionalDirect connected account ID (ca\_...)
statestroptionalOpaque value passed through to the redirect URL
user\_verify\_urlstroptionalApp redirect URL for user verification
Response schema MagicLinkResponse
Field Type Description
link str OAuth magic link URL. Redirect the user here to start the authorization flow.
expiry datetime Link expiry timestamp
Example
```python
1
magic_link = actions.get_authorization_link(
2
identifier="user@example.com",
3
connection_name="gmail",
4
user_verify_url="https://your-app.com/verify",
5
)
6
# Redirect the user to magic_link.link
```
#### verify\_connected\_account\_user
[Section titled “verify\_connected\_account\_user”](#verify_connected_account_user)
Verifies the user after OAuth callback. Call this from your redirect URL handler.
Input schema
NameTypeRequiredDescription
auth\_request\_idstrrequiredToken from the redirect URL query params
identifierstrrequiredCurrent user identifier
Response schema VerifyConnectedAccountUserResponse
Field Type Description
post\_user\_verify\_redirect\_url str URL to redirect the user to after successful verification
Example
```python
1
result = actions.verify_connected_account_user(
2
auth_request_id=request.args["auth_request_id"],
3
identifier="user@example.com",
4
)
5
# Redirect to result.post_user_verify_redirect_url
```
***
### Connected accounts
[Section titled “Connected accounts”](#connected-accounts)
#### get\_or\_create\_connected\_account
[Section titled “get\_or\_create\_connected\_account”](#get_or_create_connected_account)
Fetches an existing connected account or creates one if none exists. Use this as the default when setting up a user.
Input schema
NameTypeRequiredDescription
connection\_namestrrequiredConnector slug
identifierstrrequiredUser's identifier
authorization\_detailsdictoptionalOAuth token or static auth details
organization\_idstroptionalOrganization tenant ID when your app scopes auth and accounts by org
user\_idstroptionalYour application user ID when you map Scalekit accounts to internal users
api\_configdictoptionalConnector-specific options (for example scopes or static auth fields)
Response schema CreateConnectedAccountResponse
Field Type Description
connected\_account.id str Account ID (ca\_...)
connected\_account.identifier str User's identifier
connected\_account.provider str Provider slug
connected\_account.status str ACTIVE, INACTIVE, or PENDING
connected\_account.authorization\_type str OAuth, API\_KEY, etc.
connected\_account.token\_expires\_at datetime OAuth token expiry
Example
```python
1
account = actions.get_or_create_connected_account(
2
connection_name="gmail",
3
identifier="user@example.com",
4
)
5
print(account.connected_account.id)
```
#### get\_connected\_account
[Section titled “get\_connected\_account”](#get_connected_account)
Fetches auth details for a connected account. Returns sensitive credentials. Protect access to this method.
Requires `connected_account_id` **or** `connection_name` + `identifier`.
Input schema
NameTypeRequiredDescription
connection\_namestroptionalConnector slug. Use with identifier when you do not pass connected\_account\_id.
identifierstroptionalEnd-user or workspace identifier. Use with connection\_name.
connected\_account\_idstroptionalConnected account ID (ca\_...) when resolving by ID instead of name + identifier
Response schema GetConnectedAccountAuthResponse
Field Type Description
connected\_account.id str Account ID (ca\_...)
connected\_account.identifier str User's identifier
connected\_account.provider str Provider slug
connected\_account.status str ACTIVE, INACTIVE, or PENDING
connected\_account.authorization\_type str OAuth, API\_KEY, etc.
connected\_account.authorization\_details dict Credential payload (access token, API key, etc.)
connected\_account.token\_expires\_at datetime OAuth token expiry
connected\_account.last\_used\_at datetime Last time this account was used
connected\_account.updated\_at datetime Last update timestamp
#### list\_connected\_accounts
[Section titled “list\_connected\_accounts”](#list_connected_accounts)
Input schema
NameTypeRequiredDescription
connection\_namestroptionalFilter by connector
identifierstroptionalFilter by user identifier
providerstroptionalFilter by provider
Response schema ListConnectedAccountsResponse
Field Type Description
connected\_accounts list List of ConnectedAccountForList objects (excludes authorization\_details and api\_config)
total\_count int Total number of matching accounts
next\_page\_token str Token for the next page, if any
previous\_page\_token str Token for the previous page, if any
#### create\_connected\_account
[Section titled “create\_connected\_account”](#create_connected_account)
Creates a connected account with explicit auth details.
Input schema
NameTypeRequiredDescription
connection\_namestrrequiredConnector slug. Must match a connection configured in your environment.
identifierstrrequiredStable ID for this end user or workspace (email, user\_id, or custom string)
authorization\_detailsdictrequiredOAuth token payload, API key, or other credentials for this connector
organization\_idstroptionalOrganization tenant ID when your app scopes auth and accounts by org
user\_idstroptionalYour application user ID when you map Scalekit accounts to internal users
api\_configdictoptionalConnector-specific options (for example scopes or static auth fields)
Returns CreateConnectedAccountResponse. Same shape as `get_or_create_connected_account`.
#### update\_connected\_account
[Section titled “update\_connected\_account”](#update_connected_account)
Requires `connected_account_id` **or** `connection_name` + `identifier`.
Input schema
NameTypeRequiredDescription
connection\_namestroptionalConnector slug. Use with identifier when you do not pass connected\_account\_id.
identifierstroptionalEnd-user or workspace identifier. Use with connection\_name.
connected\_account\_idstroptionalConnected account ID (ca\_...) when updating by ID instead of name + identifier
authorization\_detailsdictoptionalReplace or merge stored credentials (OAuth tokens, API keys, etc.)
organization\_idstroptionalOrganization tenant ID when your app scopes auth and accounts by org
user\_idstroptionalYour application user ID when you map Scalekit accounts to internal users
api\_configdictoptionalConnector-specific configuration to persist on the account
Returns UpdateConnectedAccountResponse.
#### delete\_connected\_account
[Section titled “delete\_connected\_account”](#delete_connected_account)
Deletes a connected account and revokes its credentials. Requires `connected_account_id` **or** `connection_name` + `identifier`.
Input schema
NameTypeRequiredDescription
connection\_namestroptionalConnector slug. Use with identifier when you do not pass connected\_account\_id.
identifierstroptionalEnd-user or workspace identifier. Use with connection\_name.
connected\_account\_idstroptionalConnected account ID (ca\_...) when deleting by ID instead of name + identifier
Returns DeleteConnectedAccountResponse.
***
### Tool execution
[Section titled “Tool execution”](#tool-execution)
#### execute\_tool
[Section titled “execute\_tool”](#execute_tool)
Executes a named tool via Scalekit. Pre- and post-modifiers run automatically if registered.
Input schema
NameTypeRequiredDescription
tool\_namestrrequiredTool name (e.g. gmail\_fetch\_emails)
tool\_inputdictrequiredParameters the tool expects
identifierstroptionalUser's identifier
connected\_account\_idstroptionalDirect connected account ID
Response schema ExecuteToolResponse
Field Type Description
data dict Tool structured output
execution\_id str Unique ID for this execution
Example
```python
1
result = actions.execute_tool(
2
tool_name="gmail_fetch_emails",
3
tool_input={"max_results": 5, "label": "UNREAD"},
4
identifier="user@example.com",
5
)
6
emails = result.data
```
***
### Proxied API calls
[Section titled “Proxied API calls”](#proxied-api-calls)
#### request
[Section titled “request”](#request)
Makes a REST API call on behalf of a connected account. Scalekit injects the user’s OAuth token automatically.
Input schema
NameTypeRequiredDescription
connection\_namestrrequiredConnector slug
identifierstrrequiredUser's identifier
pathstrrequiredAPI path (e.g. /gmail/v1/users/me/messages)
methodstroptionalHTTP method. Default: GET
query\_paramsdictoptionalURL query parameters appended to path
bodyanyoptionalJSON-serializable body for POST, PUT, PATCH, or similar methods
form\_datadictoptionalMultipart form fields when the upstream API expects form data instead of JSON
headersdictoptionalExtra HTTP headers merged with Scalekit-injected auth headers
Returns `requests.Response`. Use `.json()`, `.status_code`, and standard response attributes.
Example
```python
1
response = actions.request(
2
connection_name="gmail",
3
identifier="user@example.com",
4
path="/gmail/v1/users/me/messages",
5
query_params={"maxResults": 5, "q": "is:unread"},
6
)
7
messages = response.json()["messages"]
```
***
## MCP server provisioning
[Section titled “MCP server provisioning”](#mcp-server-provisioning)
`actions.mcp` generates per-user MCP-compatible server URLs. Any MCP-compatible agent framework (LangChain, Google ADK, Anthropic, OpenAI, and others) can connect to these URLs directly.
**Two-step model:** Create a **config** once (defines which connectors and tools to expose), then call `ensure_instance` per user to get their personal MCP server URL.
### Configs
[Section titled “Configs”](#configs)
#### actions.mcp.create\_config
[Section titled “actions.mcp.create\_config”](#actionsmcpcreate_config)
Input schema
NameTypeRequiredDescription
namestrrequiredConfig name
descriptionstroptionalHuman-readable summary of what this MCP config exposes
connection\_tool\_mappingslistoptionalList of McpConfigConnectionToolMapping objects
Response schema CreateMcpConfigResponse
Field Type Description
config.id str Config ID
config.name str Config name
config.connection\_tool\_mappings list Connector-to-tools mappings
Example
```python
1
from scalekit.actions.types import McpConfigConnectionToolMapping
2
3
config = actions.mcp.create_config(
4
name="email-agent",
5
connection_tool_mappings=[
6
McpConfigConnectionToolMapping(
7
connection_name="gmail",
8
tools=["gmail_fetch_emails", "gmail_send_email"],
9
)
10
],
11
)
```
#### actions.mcp.list\_configs
[Section titled “actions.mcp.list\_configs”](#actionsmcplist_configs)
Input schema
NameTypeRequiredDescription
page\_sizeintoptionalMaximum configs per page (server default if omitted)
page\_tokenstroptionalOpaque cursor from a previous list response
filter\_namestroptionalFilter by exact name
filter\_providerstroptionalFilter by provider slug
searchstroptionalFree-text search on name
Returns ListMcpConfigsResponse.
#### actions.mcp.update\_config
[Section titled “actions.mcp.update\_config”](#actionsmcpupdate_config)
Input schema
NameTypeRequiredDescription
config\_idstrrequiredMCP config ID from create\_config or list\_configs
descriptionstroptionalNew human-readable description for this config
connection\_tool\_mappingslistoptionalReplaces existing mappings
Returns UpdateMcpConfigResponse.
#### actions.mcp.delete\_config
[Section titled “actions.mcp.delete\_config”](#actionsmcpdelete_config)
Input schema
NameTypeRequiredDescription
config\_idstrrequiredMCP config ID to delete
Returns DeleteMcpConfigResponse.
### Instances
[Section titled “Instances”](#instances)
#### actions.mcp.ensure\_instance
[Section titled “actions.mcp.ensure\_instance”](#actionsmcpensure_instance)
Creates an MCP instance for this user if one doesn’t exist, or returns the existing one. Call this on every session; it’s idempotent.
The `instance.url` field is the MCP server URL to give to the user’s agent or IDE.
Input schema
NameTypeRequiredDescription
config\_namestrrequiredName of the config to instantiate
user\_identifierstrrequiredUser identifier (e.g. email)
namestroptionalDisplay name for the instance
Response schema EnsureMcpInstanceResponse
Field Type Description
instance.url str MCP server URL for agent or IDE
instance.id str Instance ID
instance.name str Display name
instance.user\_identifier str User identifier
instance.config object The config this instance was created from
instance.last\_used\_at datetime Last usage timestamp
instance.updated\_at datetime Last update timestamp
Example
```python
1
instance = actions.mcp.ensure_instance(
2
config_name="email-agent",
3
user_identifier="user@example.com",
4
)
5
mcp_url = instance.instance.url
6
# Give mcp_url to the user's agent or IDE
```
#### actions.mcp.get\_instance\_auth\_state
[Section titled “actions.mcp.get\_instance\_auth\_state”](#actionsmcpget_instance_auth_state)
Returns authorization status per connector. Use `include_auth_links=True` to generate fresh auth links for connections that need authorization or re-authorization.
Input schema
NameTypeRequiredDescription
instance\_idstrrequiredInstance ID
include\_auth\_linksbooloptionalGenerate auth links for unauthorized connections
Response schema GetMcpInstanceAuthStateResponse
Field Type Description
connections list List of McpInstanceConnectionAuthState, one per configured connector
connections\[].connection\_id str Connection ID
connections\[].connection\_name str Connector slug
connections\[].provider str Provider slug
connections\[].connected\_account\_id str Connected account ID, if authorized
connections\[].connected\_account\_status str ACTIVE, INACTIVE, or PENDING
connections\[].authentication\_link str Auth link to send to the user when status is not ACTIVE
Example
```python
1
auth_state = actions.mcp.get_instance_auth_state(
2
instance_id=instance.instance.id,
3
include_auth_links=True,
4
)
5
for conn in auth_state.connections:
6
if conn.connected_account_status != "ACTIVE":
7
# Send conn.authentication_link to the user to authorize
8
print(f"{conn.connection_name}: {conn.authentication_link}")
```
#### actions.mcp.get\_instance
[Section titled “actions.mcp.get\_instance”](#actionsmcpget_instance)
Input schema
NameTypeRequiredDescription
instance\_idstrrequiredMCP instance ID from ensure\_instance or list\_instances
Returns GetMcpInstanceResponse.
#### actions.mcp.list\_instances
[Section titled “actions.mcp.list\_instances”](#actionsmcplist_instances)
Input schema
NameTypeRequiredDescription
page\_sizeintoptionalMaximum instances per page (server default if omitted)
page\_tokenstroptionalOpaque cursor from a previous list response
filter\_user\_identifierstroptionalFilter by user
filter\_config\_namestroptionalFilter by config name
filter\_namestroptionalFilter by MCP instance display name
filter\_idstroptionalFilter by MCP instance ID
Returns ListMcpInstancesResponse.
#### actions.mcp.update\_instance
[Section titled “actions.mcp.update\_instance”](#actionsmcpupdate_instance)
At least one of `name` or `config_name` is required.
Input schema
NameTypeRequiredDescription
instance\_idstrrequiredMCP instance ID to update
namestroptionalNew display name for this instance
config\_namestroptionalSwitch the instance to a different config by name
Returns UpdateMcpInstanceResponse.
#### actions.mcp.delete\_instance
[Section titled “actions.mcp.delete\_instance”](#actionsmcpdelete_instance)
Input schema
NameTypeRequiredDescription
instance\_idstrrequiredMCP instance ID to delete
Returns DeleteMcpInstanceResponse.
***
## Framework adapters
[Section titled “Framework adapters”](#framework-adapters)
Pre-built integrations for LangChain and Google ADK. Use these when your agent runs in one of these frameworks and you prefer native tool objects over an MCP URL.
MCP is the recommended path
`actions.mcp.ensure_instance` generates a URL compatible with any MCP-supporting framework. Use framework adapters only when native tool objects are required.
### LangChain
[Section titled “LangChain”](#langchain)
```bash
1
pip install langchain
```
#### actions.langchain.get\_tools
[Section titled “actions.langchain.get\_tools”](#actionslangchainget_tools)
Input schema
NameTypeRequiredDescription
identifierstrrequiredUser connected account identifier
providerslistoptionalFilter by provider (e.g. \["google"])
tool\_nameslistoptionalFilter by tool name
connection\_nameslistoptionalFilter by connection name
page\_sizeintoptionalMaximum tools per page (server default if omitted)
page\_tokenstroptionalOpaque cursor from a previous list response
Response schema List\[StructuredTool]
Field Type Description
\[].name str Tool name
\[].description str Tool description
\[].args\_schema object Pydantic schema for the tool inputs
Example
```python
1
from langchain.agents import create_react_agent
2
3
tools = actions.langchain.get_tools(identifier="user@example.com")
4
agent = create_react_agent(llm=llm, tools=tools, prompt=prompt)
```
### Google ADK
[Section titled “Google ADK”](#google-adk)
```bash
1
pip install google-adk
```
#### actions.google.get\_tools
[Section titled “actions.google.get\_tools”](#actionsgoogleget_tools)
Same parameters as `actions.langchain.get_tools`.
Returns `List[ScalekitGoogleAdkTool]`. Pass it directly to a Google ADK agent.
Example
```python
1
tools = actions.google.get_tools(identifier="user@example.com")
```
***
## Tools client
[Section titled “Tools client”](#tools-client)
`scalekit_client.actions.tools` gives access to raw tool schemas. Use this when building a custom adapter or passing schemas directly to an LLM API (e.g. Anthropic, OpenAI).
#### actions.tools.list\_tools
[Section titled “actions.tools.list\_tools”](#actionstoolslist_tools)
Input schema
NameTypeRequiredDescription
filterFilteroptionalFilter by provider, identifier, or tool name
page\_sizeintoptionalMaximum tools per page (server default if omitted)
page\_tokenstroptionalOpaque cursor from a previous list response
Response schema ListToolsResponse
Field Type Description
tools list List of tool schemas (name, description, input schema)
next\_page\_token str Token for the next page, if any
#### actions.tools.list\_scoped\_tools
[Section titled “actions.tools.list\_scoped\_tools”](#actionstoolslist_scoped_tools)
Lists tools scoped to a specific user. This is what framework adapters use internally to fetch per-user tool schemas.
Input schema
NameTypeRequiredDescription
identifierstrrequiredUser connected account identifier
filterScopedToolFilteroptionalFilter by providers, tool names, or connection names
page\_sizeintoptionalMaximum tools per page (server default if omitted)
page\_tokenstroptionalOpaque cursor from a previous list response
Response schema ListScopedToolsResponse
Field Type Description
tools list List of tool schemas (name, description, input\_schema)
tools\[].name str Tool name
tools\[].description str Tool description
tools\[].input\_schema object JSON Schema for tool inputs. Pass directly to LLM API.
next\_page\_token str Token for the next page, if any
Example
```python
1
tools_response = scalekit_client.actions.tools.list_scoped_tools(
2
identifier="user@example.com",
3
)
4
# Pass tools_response.tools to your LLM's tool call API
```
#### actions.tools.execute\_tool
[Section titled “actions.tools.execute\_tool”](#actionstoolsexecute_tool)
Low-level tool execution. Bypasses modifiers. Prefer `actions.execute_tool` in most cases.
Input schema
NameTypeRequiredDescription
tool\_namestrrequiredRegistered tool name to execute
identifierstrrequiredEnd-user or workspace identifier used to resolve the connected account
paramsdictoptionalTool arguments matching the tool input schema
connected\_account\_idstroptionalConnected account ID (ca\_...) when you already know it
Returns ExecuteToolResponse. Same shape as `actions.execute_tool`.
***
## Modifiers
[Section titled “Modifiers”](#modifiers)
Modifiers intercept tool calls to transform inputs or outputs, useful for validation, enrichment, or logging.
```python
1
@actions.pre_modifier(tool_names=["gmail_fetch_emails"])
2
def add_default_label(tool_input):
3
tool_input.setdefault("label", "UNREAD")
4
return tool_input
5
6
@actions.post_modifier(tool_names=["gmail_fetch_emails"])
7
def filter_attachments(tool_output):
8
tool_output["emails"] = [e for e in tool_output["emails"] if not e.get("has_attachment")]
9
return tool_output
```
| Decorator | Receives | Returns |
| ------------------------------------ | -------- | --------------- |
| `@actions.pre_modifier(tool_names)` | `dict` | Modified `dict` |
| `@actions.post_modifier(tool_names)` | `dict` | Modified `dict` |
`tool_names` accepts a string or a list of strings. Multiple modifiers for the same tool chain in registration order.
***
## Error handling
[Section titled “Error handling”](#error-handling)
```python
1
from scalekit.common.exceptions import ScalekitNotFoundException, ScalekitServerException
2
3
try:
4
account = actions.get_connected_account(
5
connection_name="gmail",
6
identifier="user@example.com",
7
)
8
except ScalekitNotFoundException:
9
# Account does not exist: create it or redirect to auth
10
pass
11
except ScalekitServerException as e:
12
print(e.error_code, e.http_status)
```
| Exception | When raised |
| ------------------------------- | -------------------------------- |
| `ScalekitNotFoundException` | Resource not found |
| `ScalekitUnauthorizedException` | Invalid credentials |
| `ScalekitForbiddenException` | Insufficient permissions |
| `ScalekitServerException` | Base class for all server errors |
---
# DOCUMENT BOUNDARY
---
# Authorize a user
> Generate an authorization link, send it to your user, and confirm their connected account is active before your agent executes tools.
Once a connection is configured, your users need to grant your agent access to their account. This happens once per user per connection. Scalekit stores their tokens and keeps them fresh automatically.
The flow is:
1. Create a connected account for the user
2. Generate an authorization link and send it to the user
3. The user completes the OAuth consent screen
4. The connected account becomes `ACTIVE`. Your agent can now execute tools.
## Create a connected account and generate a link
[Section titled “Create a connected account and generate a link”](#create-a-connected-account-and-generate-a-link)
* Python
```python
1
# Create or retrieve the connected account for this user
2
response = actions.get_or_create_connected_account(
3
connection_name="gmail",
4
identifier="user_123" # your app's unique user ID
5
)
6
connected_account = response.connected_account
7
8
# Generate the authorization link if the account is not yet active
9
if connected_account.status != "ACTIVE":
10
link_response = actions.get_authorization_link(
11
connection_name="gmail",
12
identifier="user_123"
13
)
14
auth_url = link_response.link
15
# Redirect or send auth_url to the user
```
* Node.js
```typescript
1
import { ConnectorStatus } from '@scalekit-sdk/node/lib/pkg/grpc/scalekit/v1/connected_accounts/connected_accounts_pb';
2
3
// Create or retrieve the connected account for this user
4
const response = await actions.getOrCreateConnectedAccount({
5
connectionName: 'gmail',
6
identifier: 'user_123', // your app's unique user ID
7
});
8
9
const connectedAccount = response.connectedAccount;
10
11
// Generate the authorization link if the account is not yet active
12
if (connectedAccount?.status !== ConnectorStatus.ACTIVE) {
13
const linkResponse = await actions.getAuthorizationLink({
14
connectionName: 'gmail',
15
identifier: 'user_123',
16
});
17
const authUrl = linkResponse.link;
18
// Redirect or send authUrl to the user
19
}
```
## Send the link to the user
[Section titled “Send the link to the user”](#send-the-link-to-the-user)
How you deliver the link depends on your application:
* **Web app:** redirect the user to `auth_url` directly if they’re in an active browser session
* **Email or notification:** send the link when the user isn’t actively in your app, or when connecting at their own pace is acceptable
* **In-app prompt:** show a button (“Connect Gmail”) when you want to prompt connection at a specific moment in the user’s workflow
Once the user opens the link and approves the OAuth consent screen, Scalekit exchanges the authorization code for tokens and marks the connected account `ACTIVE`. You do not need to handle the OAuth callback yourself.
Production: add user verification
By default, any user who completes the OAuth flow activates the connected account. In production, verify that the authorizing user matches the user your app intended to connect. See [Verify user identity](/agentkit/user-verification/).
## Check status and re-authorize
[Section titled “Check status and re-authorize”](#check-status-and-re-authorize)
Check the connected account status before executing tools. Tokens can expire or be revoked, so generate a new authorization link using the same flow when that happens.
* Python
```python
1
response = actions.get_or_create_connected_account(
2
connection_name="gmail",
3
identifier="user_123"
4
)
5
connected_account = response.connected_account
6
# ACTIVE: ready for tool calls
7
# PENDING: user has not completed the OAuth flow
8
# EXPIRED: tokens expired, re-authorization required
9
# REVOKED: user revoked access from the provider
10
11
if connected_account.status != "ACTIVE":
12
link_response = actions.get_authorization_link(
13
connection_name="gmail",
14
identifier="user_123"
15
)
16
# Redirect or send link_response.link to the user
```
* Node.js
```typescript
1
import { ConnectorStatus } from '@scalekit-sdk/node/lib/pkg/grpc/scalekit/v1/connected_accounts/connected_accounts_pb';
2
3
const response = await actions.getOrCreateConnectedAccount({
4
connectionName: 'gmail',
5
identifier: 'user_123',
6
});
7
8
const connectedAccount = response.connectedAccount;
9
// ACTIVE: ready for tool calls
10
// PENDING: user has not completed the OAuth flow
11
// EXPIRED: tokens expired, re-authorization required
12
// REVOKED: user revoked access from the provider
13
14
if (connectedAccount?.status !== ConnectorStatus.ACTIVE) {
15
const linkResponse = await actions.getAuthorizationLink({
16
connectionName: 'gmail',
17
identifier: 'user_123',
18
});
19
// Redirect or send linkResponse.link to the user
20
}
```
---
# 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
> Build tools that Scalekit does not provide out of the box by proxying provider API calls through connected accounts.
When you need a connector tool that Scalekit doesn’t offer as a pre-built tool, use **API Proxy mode**. You define the tool contract and call the provider endpoint through `actions.request`. Scalekit injects the user’s credentials from their connected account; your agent never handles raw tokens.
| Option | Best for | Who defines tool schema |
| ------------------------ | --------------------------------- | ----------------------- |
| Scalekit optimized tools | Common connector tools | Scalekit |
| Custom tools (API Proxy) | Unsupported or app-specific tools | Your application |
This page assumes the user has an `ACTIVE` connected account. If not, see [Authorize a user](/agentkit/tools/authorize/).
## Find the right endpoint
[Section titled “Find the right endpoint”](#find-the-right-endpoint)
The `path` you pass to `actions.request` is forwarded directly to the provider’s API; Scalekit only adds authentication headers. Look up the provider’s API reference to get the correct path, method, and request shape.
| Connector | API reference |
| ---------- | ------------------------------------------------------------------------------------------------ |
| Gmail | [Google Gmail API](https://developers.google.com/gmail/api/reference/rest) |
| Slack | [Slack API methods](https://api.slack.com/methods) |
| GitHub | [GitHub REST API](https://docs.github.com/en/rest) |
| Salesforce | [Salesforce REST API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/) |
| HubSpot | [HubSpot API](https://developers.hubspot.com/docs/api/overview) |
Base URL is managed by Scalekit
Provide only the path; Scalekit resolves the correct base URL for the connector and injects the user’s credentials automatically.
## Define your tool contract
[Section titled “Define your tool contract”](#define-your-tool-contract)
Design the tool around your agent’s intent, not the provider’s API surface. For example, to list Gmail filters:
* **Tool name:** `gmail_list_filters` (describes the action, not the endpoint)
* **Input:** `identifier` (your app’s user ID)
* **Output:** `{ filters: [...], count: N }` (structured, not the raw Gmail response)
Keep schemas focused on what the model needs. Strip provider-specific noise before returning data.
## Proxy the API call
[Section titled “Proxy the API call”](#proxy-the-api-call)
Use `actions.request` to call any provider endpoint. Scalekit handles credential injection.
**GET requests:** pass query parameters as a dict:
* Python
```python
1
def gmail_list_filters(identifier: str):
2
response = actions.request(
3
connection_name="gmail",
4
identifier=identifier,
5
method="GET",
6
path="/gmail/v1/users/me/settings/filters",
7
)
8
data = response.json()
9
return {"filters": data.get("filter", []), "count": len(data.get("filter", []))}
10
11
def gmail_list_unread(identifier: str, max_results: int = 10):
12
response = actions.request(
13
connection_name="gmail",
14
identifier=identifier,
15
method="GET",
16
path="/gmail/v1/users/me/messages",
17
query_params={"q": "is:unread", "maxResults": max_results},
18
)
19
return {"messages": response.json().get("messages", [])}
```
* Node.js
```typescript
1
async function gmailListFilters(identifier: string) {
2
const response = await scalekit.actions.request({
3
connectionName: 'gmail',
4
identifier,
5
method: 'GET',
6
path: '/gmail/v1/users/me/settings/filters',
7
});
8
const filters = response.data?.filter ?? [];
9
return { filters, count: filters.length };
10
}
11
12
async function gmailListUnread(identifier: string, maxResults = 10) {
13
const response = await scalekit.actions.request({
14
connectionName: 'gmail',
15
identifier,
16
method: 'GET',
17
path: '/gmail/v1/users/me/messages',
18
queryParams: { q: 'is:unread', maxResults },
19
});
20
return { messages: response.data?.messages ?? [] };
21
}
```
**POST requests:** pass a body for write operations:
* Python
```python
1
def slack_send_message(identifier: str, channel: str, text: str):
2
response = actions.request(
3
connection_name="slack",
4
identifier=identifier,
5
method="POST",
6
path="/api/chat.postMessage",
7
body={"channel": channel, "text": text},
8
)
9
data = response.json()
10
if not data.get("ok"):
11
raise ValueError(f"Slack error: {data.get('error')}")
12
return {"ts": data.get("ts"), "channel": data.get("channel")}
```
* Node.js
```typescript
1
async function slackSendMessage(identifier: string, channel: string, text: string) {
2
const response = await scalekit.actions.request({
3
connectionName: 'slack',
4
identifier,
5
method: 'POST',
6
path: '/api/chat.postMessage',
7
body: { channel, text },
8
});
9
if (!response.data?.ok) throw new Error(`Slack error: ${response.data?.error}`);
10
return { ts: response.data.ts, channel: response.data.channel };
11
}
```
## Check authorization before proxy calls
[Section titled “Check authorization before proxy calls”](#check-authorization-before-proxy-calls)
Verify the connected account is `ACTIVE` before making a proxy call and handle provider errors explicitly:
* Python
```python
1
account = actions.get_or_create_connected_account(
2
connection_name="gmail",
3
identifier=identifier,
4
).connected_account
5
6
if account.status != "ACTIVE":
7
raise ValueError("Connected account is not ACTIVE. Re-authorize the user.")
```
* Node.js
```typescript
1
import { ConnectorStatus } from '@scalekit-sdk/node/lib/pkg/grpc/scalekit/v1/connected_accounts/connected_accounts_pb';
2
3
const account = (await scalekit.actions.getOrCreateConnectedAccount({
4
connectionName: 'gmail',
5
identifier,
6
})).connectedAccount;
7
8
if (account?.status !== ConnectorStatus.ACTIVE) {
9
throw new Error('Connected account is not ACTIVE. Re-authorize the user.');
10
}
```
## Best practices
[Section titled “Best practices”](#best-practices)
* Expose only the fields your model needs; keep schemas small
* Validate inputs server-side; never trust model-generated parameters
* Use predictable JSON keys; return stable output across calls
* Map provider errors to clear tool errors; don’t leak raw provider payloads to prompts
---
# 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
---
# Scalekit optimized built-in tools
> Call Scalekit's pre-built tools across 60+ connectors. Each tool returns structured, LLM-ready output with no endpoint URLs, auth headers, or parsing needed.
Scalekit ships pre-built tools for every connector in the catalog: Gmail, Slack, GitHub, Salesforce, Notion, Linear, HubSpot, and more. Each tool has an LLM-ready schema and returns structured output. Your agent passes inputs; Scalekit injects the user’s credentials and handles the API call.
This page assumes you have an `ACTIVE` connected account for the user. If not, see [Authorize a user](/agentkit/tools/authorize/).
## Find available tools
[Section titled “Find available tools”](#find-available-tools)
Use `list_scoped_tools` / `listScopedTools` to get the tools this specific user is authorized to call. **This is the list you pass to your LLM.**
* Python
```python
1
from google.protobuf.json_format import MessageToDict
2
3
scoped_response, _ = actions.tools.list_scoped_tools(
4
identifier="user_123",
5
filter={"connection_names": ["gmail"]}, # optional; omit for all connectors
6
)
7
for scoped_tool in scoped_response.tools:
8
definition = MessageToDict(scoped_tool.tool).get("definition", {})
9
print(definition.get("name"))
10
print(definition.get("input_schema")) # JSON Schema; pass directly to your LLM
```
* Node.js
```typescript
1
const { tools } = await scalekit.tools.listScopedTools('user_123', {
2
filter: { connectionNames: ['gmail'] }, // optional; omit for all connectors
3
});
4
for (const tool of tools) {
5
const { name, input_schema } = tool.tool.definition;
6
console.log(name, input_schema); // JSON Schema; pass directly to your LLM
7
}
```
To browse all available tools without filtering by user, use `list_tools` / `listTools`. To explore tools interactively and inspect live response shapes, use the playground at **app.scalekit.com > Agent Auth > Playground**.
## Execute a tool
[Section titled “Execute a tool”](#execute-a-tool)
`execute_tool` / `executeTool` runs a named tool for a specific user. The same pattern works across every connector. Swap `tool_name` and `tool_input`:
| Connector | Tool name | Sample `tool_input` |
| ------------ | ------------------------ | ------------------------------------------------------------------------------ |
| `gmail` | `gmail_fetch_mails` | `{ "query": "is:unread", "max_results": 5 }` |
| `slack` | `slack_send_message` | `{ "channel": "#general", "text": "Hello" }` |
| `github` | `github_create_issue` | `{ "repo": "acme/app", "title": "Bug", "body": "…" }` |
| `salesforce` | `salesforce_create_lead` | `{ "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com" }` |
Tool names are illustrative
Exact tool names and input schemas vary by connector. Call `list_scoped_tools` or check the connector page in the dashboard for the current schema.
* Python
```python
1
result = actions.execute_tool(
2
tool_name="gmail_fetch_mails",
3
identifier="user_123",
4
tool_input={"query": "is:unread", "max_results": 5},
5
)
6
print(result.data)
```
* Node.js
```typescript
1
const result = await scalekit.actions.executeTool({
2
toolName: 'gmail_fetch_mails',
3
identifier: 'user_123',
4
toolInput: { query: 'is:unread', max_results: 5 },
5
});
6
console.log(result.data);
```
## Wire into your LLM
[Section titled “Wire into your LLM”](#wire-into-your-llm)
The full agent loop: fetch scoped tools → pass to LLM → execute tool calls → feed results back.
* Python
```python
1
import anthropic
2
from google.protobuf.json_format import MessageToDict
3
4
client = anthropic.Anthropic()
5
6
# 1. Fetch tools scoped to this user
7
scoped_response, _ = actions.tools.list_scoped_tools(
8
identifier="user_123",
9
filter={"connection_names": ["gmail"]},
10
)
11
llm_tools = [
12
{
13
"name": MessageToDict(t.tool).get("definition", {}).get("name"),
14
"description": MessageToDict(t.tool).get("definition", {}).get("description"),
15
"input_schema": MessageToDict(t.tool).get("definition", {}).get("input_schema", {}),
16
}
17
for t in scoped_response.tools
18
]
19
20
# 2. Send to LLM
21
messages = [{"role": "user", "content": "Summarize my last 5 unread emails"}]
22
response = client.messages.create(
23
model="claude-sonnet-4-6",
24
max_tokens=1024,
25
tools=llm_tools,
26
messages=messages,
27
)
28
29
# 3. Execute tool calls and feed results back
30
for block in response.content:
31
if block.type == "tool_use":
32
tool_result = actions.execute_tool(
33
tool_name=block.name,
34
identifier="user_123",
35
tool_input=block.input,
36
)
37
messages.append({"role": "assistant", "content": response.content})
38
messages.append({
39
"role": "user",
40
"content": [{"type": "tool_result", "tool_use_id": block.id, "content": str(tool_result.data)}],
41
})
```
* Node.js
```typescript
1
import Anthropic from '@anthropic-ai/sdk';
2
3
const anthropic = new Anthropic();
4
5
// 1. Fetch tools scoped to this user
6
const { tools } = await scalekit.tools.listScopedTools('user_123', {
7
filter: { connectionNames: ['gmail'] },
8
});
9
const llmTools = tools.map((t) => ({
10
name: t.tool.definition.name,
11
description: t.tool.definition.description,
12
input_schema: t.tool.definition.input_schema,
13
}));
14
15
// 2. Send to LLM
16
const messages: Anthropic.MessageParam[] = [
17
{ role: 'user', content: 'Summarize my last 5 unread emails' },
18
];
19
const response = await anthropic.messages.create({
20
model: 'claude-sonnet-4-6',
21
max_tokens: 1024,
22
tools: llmTools,
23
messages,
24
});
25
26
// 3. Execute tool calls and feed results back
27
for (const block of response.content) {
28
if (block.type === 'tool_use') {
29
const toolResult = await scalekit.actions.executeTool({
30
toolName: block.name,
31
identifier: 'user_123',
32
toolInput: block.input as Record,
33
});
34
messages.push({ role: 'assistant', content: response.content });
35
messages.push({
36
role: 'user',
37
content: [{ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(toolResult.data) }],
38
});
39
}
40
}
```
## Use a framework adapter
[Section titled “Use a framework adapter”](#use-a-framework-adapter)
For LangChain and Google ADK, Scalekit returns native tool objects in Python with no schema reshaping needed.
* LangChain
```python
1
from langchain_openai import ChatOpenAI
2
from langchain.agents import create_agent
3
4
tools = actions.langchain.get_tools(
5
identifier="user_123",
6
providers=["GMAIL"],
7
page_size=100,
8
)
9
llm = ChatOpenAI(model="claude-sonnet-4-6")
10
agent = create_agent(model=llm, tools=tools, system_prompt="You are a helpful assistant.")
11
result = agent.invoke({"messages": [{"role": "user", "content": "Fetch my last 5 unread emails"}]})
```
* Google ADK
```python
1
from google.adk.agents import Agent
2
from google.adk.models.lite_llm import LiteLlm
3
4
gmail_tools = actions.google.get_tools(
5
identifier="user_123",
6
providers=["GMAIL"],
7
page_size=100,
8
)
9
agent = Agent(
10
name="gmail_assistant",
11
model=LiteLlm(model="claude-sonnet-4-6"),
12
tools=gmail_tools,
13
)
```
* Node.js (Vercel AI SDK)
```typescript
1
import { generateText, jsonSchema, tool } from 'ai';
2
3
const { tools: scopedTools } = await scalekit.tools.listScopedTools('user_123', {
4
filter: { connectionNames: ['gmail'] },
5
});
6
const tools = Object.fromEntries(
7
scopedTools.map((t) => [
8
t.tool.definition.name,
9
tool({
10
description: t.tool.definition.description,
11
parameters: jsonSchema(t.tool.definition.input_schema ?? { type: 'object', properties: {} }),
12
execute: async (args) => {
13
const result = await scalekit.actions.executeTool({
14
toolName: t.tool.definition.name,
15
toolInput: args,
16
identifier: 'user_123',
17
});
18
return result.data;
19
},
20
}),
21
]),
22
);
```
MCP-compatible frameworks
Prefer a single interface any MCP client can consume? See [Configure an MCP server](/agentkit/mcp/configure-mcp-server/).
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
Connected account stays in `PENDING`
The user hasn’t completed the OAuth flow yet. Call `get_authorization_link` and redirect the user to the link. Retry after consent completes.
Tool call fails with resource not found
Check three things:
* The connector name exists in **Agent Auth > Connections**
* The `identifier` matches the one used when creating the connected account
* Call `list_scoped_tools` and only execute tool names it returns
Connection names differ across environments
Connection names are workspace-specific. Don’t hard-code them. Use environment variables (`GMAIL_CONNECTION_NAME`, `GITHUB_CONNECTION_NAME`) and reference those in API calls.
If you need an endpoint not covered by optimized tools, see [Custom tools](/agentkit/tools/custom-tools/).
---
# DOCUMENT BOUNDARY
---
# Verify user identity
> Confirm that the user who completed the OAuth consent is the same user your app intended to connect.
User verification applies to OAuth-based connectors only. For API key, basic auth, and key pair connectors, the user provides credentials directly. No OAuth flow, no verification step needed.
For OAuth connectors, before activating a connected account, Scalekit confirms that the user who completed the OAuth consent is the same user your app intended to connect. This **user verification** step runs every time a connected account is authorized and prevents OAuth consent from activating on the wrong account.
Choose a mode in **Agent Auth > User Verification**:
* **Custom user verification**: Your server confirms the authorizing user matches the user your app intended to connect. Use in production. Without this, any user who receives an authorization link can activate a connected account (including the wrong one).
* **Scalekit users only**: Scalekit checks that the authorizing user is signed in to your Scalekit dashboard. No code required. Use during development and internal testing when all users are already on your team.
Scalekit users only is for testing
In this mode, the user authorizing the connection must already be signed in to the Scalekit dashboard. No verify route or API calls are needed in your code. Switch to **Custom user verification** before onboarding real users.

Your application implements the verify step. End users never interact with Scalekit directly.
When the user finishes OAuth, Scalekit redirects to your verify URL with `auth_request_id` and `state` params. Your route reads the user from your session, calls Scalekit’s verify API with the `auth_request_id` and the original `identifier`, and if they match, the connected account activates.
Review the verification sequence
## Implement verification in your app
[Section titled “Implement verification in your app”](#implement-verification-in-your-app)
If you haven’t installed the SDK yet, see the [quickstart](/agentkit/quickstart/).
### Generate the authorization link
[Section titled “Generate the authorization link”](#generate-the-authorization-link)
Pass these fields when creating the authorization link:
| Field | Description |
| ----------------- | ------------------------------------------------------------------------------------------------- |
| `identifier` | **Required.** Your user’s ID or email. Scalekit stores this and checks it matches at verify time. |
| `user_verify_url` | **Required.** Your callback URL; Scalekit redirects the user here after OAuth completes. |
| `state` | **Recommended.** A random value to prevent CSRF. |
How to use state
Generate a cryptographically random value per flow, store it in a secure HTTP-only cookie, and validate it against the `state` query param on callback. Discard the request if they don’t match; this prevents an attacker from sending crafted verify URLs to your users.
* Python
```python
1
import secrets
2
3
# Generate a state value to prevent CSRF
4
state = secrets.token_urlsafe(32)
5
# Store state in a secure, HTTP-only cookie to validate on callback
6
7
response = scalekit_client.actions.get_authorization_link(
8
connection_name=connector,
9
identifier=user_id,
10
user_verify_url="https://app.yourapp.com/user/verify",
11
state=state,
12
)
```
* Node.js
```typescript
1
import crypto from 'node:crypto';
2
3
// Generate a state value to prevent CSRF
4
const state = crypto.randomUUID();
5
// Store state in a secure, HTTP-only cookie to validate on callback
6
7
const { link } = await scalekit.actions.getAuthorizationLink({
8
identifier: userId,
9
connectionName: connector,
10
userVerifyUrl: 'https://app.yourapp.com/user/verify',
11
state,
12
});
```
### Handle the verification callback
[Section titled “Handle the verification callback”](#handle-the-verification-callback)
After OAuth completes, Scalekit redirects to your `user_verify_url`:
```http
1
GET https://app.yourapp.com/user/verify?auth_request_id=req_xyz&state=
```
Validate `state` against your cookie, then call Scalekit’s verify endpoint server-side.
Never trust query params for identity
Read the user’s identity from your own session, not from the URL. Use `state` for session correlation only.
* Python
```python
1
# 1. Validate state from query param matches state in cookie
2
# 2. Read user identity from your session, not from the URL
3
4
response = scalekit_client.actions.verify_connected_account_user(
5
auth_request_id=auth_request_id,
6
identifier=user_id, # must match what was stored at link creation
7
)
8
# On success: redirect to response.post_user_verify_redirect_url
```
* Node.js
```typescript
1
// 1. Validate state from query param matches state in cookie
2
// 2. Read user identity from your session, not from the URL
3
4
const { postUserVerifyRedirectUrl } =
5
await scalekit.actions.verifyConnectedAccountUser({
6
authRequestId: auth_request_id,
7
identifier: userId, // must match what was stored at link creation
8
});
9
// On success: redirect to postUserVerifyRedirectUrl
```
On success, the connected account is activated. Redirect the user using `post_user_verify_redirect_url`.
---
# 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.

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”

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.

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\":\"\"}";
//