) {
27
if (toolName === 'gmail_list_filters') return gmailListFilters(toolInput.identifier);
28
if (toolName === 'slack_send_message') return slackSendMessage(toolInput.identifier, toolInput.channel, toolInput.text);
29
throw new Error(`Unknown tool: ${toolName}`);
30
}
```
## 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\":\"