Migrate from Nango
Move an agent from Nango to AgentKit: map Nango integrations and connections, export credentials and import them, and update your tool calls.
Use this page if your agent uses Nango today. It covers what’s specific to Nango: how its concepts map to AgentKit, how to export your users’ credentials and import them, and how your tool calls change. The steps every migration shares, such as setting up Scalekit and creating connections, are in Migrate to AgentKit.
How Nango maps to AgentKit
Section titled “How Nango maps to AgentKit”| Nango | AgentKit |
|---|---|
Integration (integration ID, provider_config_key) | Connection, referred to by its connection name |
Connection (connection_id) | Connected account: one per user and connection |
end_user_id tag | Identifier: your app’s ID for the user, passed on each call |
| Connect UI and connect session | Authorization link |
| Tools and actions you write | Connector tools, or custom tools that call the API through the proxy |
Proxy (nango.proxy(), nango.get()) | actions.request() through the Scalekit proxy |
| Agent sessions over MCP | Virtual MCP server with a session token per agent run |
Move your users
Section titled “Move your users”Nango returns a connection’s stored credentials, so you can import them and your users don’t have to connect again. That works when:
- OAuth: the Nango integration uses your own OAuth app. Create the Scalekit connection with the same client ID and secret; see Bring your own OAuth app. A refresh token works only with the OAuth app that issued it, so users of an integration on a Nango-managed OAuth app authorize again.
- API keys and basic auth: always.
- Your Nango secret key has the
environment:connections:read_credentialsscope. Without it, Nango leaves the credentials out of the response.
The script below lists the Nango connections of each integration, reads each one’s credentials and creates a connected account in Scalekit. It uses each connection’s end_user_id tag as the Scalekit identifier. If you keep your user ID somewhere else, such as in the connection’s metadata, change that line.
Add your Nango secret key to the .env file from Set up Scalekit:
NANGO_SECRET_KEY=your_nango_secret_keyInstall requests too: pip install scalekit-sdk-python python-dotenv requests. The script needs Python 3.11 or later.
import osfrom datetime import datetime
import requestsfrom dotenv import load_dotenvfrom scalekit import ScalekitClientfrom scalekit.common.exceptions import ScalekitServerException
load_dotenv()scalekit_client = ScalekitClient( os.environ["SCALEKIT_ENVIRONMENT_URL"], os.environ["SCALEKIT_CLIENT_ID"], os.environ["SCALEKIT_CLIENT_SECRET"],)actions = scalekit_client.actions
NANGO_URL = "https://api.nango.dev"NANGO_HEADERS = {"Authorization": f"Bearer {os.environ['NANGO_SECRET_KEY']}"}
# Your Nango integration ID -> (Scalekit connection name, field for an API key)CONNECTIONS = { "google-mail": ("gmail", None), # OAuth "openai": ("openai", "token"), # API key, sent as a bearer token "twilio": ("twilio", None), # basic auth}
def to_authorization_details(credentials, key_field): kind = credentials["type"] if kind == "OAUTH2": token = {"access_token": credentials["access_token"]} if credentials.get("refresh_token"): token["refresh_token"] = credentials["refresh_token"] if credentials.get("expires_at"): token["expires_at"] = datetime.fromisoformat(credentials["expires_at"]) return {"oauth_token": token} if kind == "API_KEY" and key_field: return {"static_auth": {key_field: credentials["apiKey"]}} if kind == "BASIC": return {"static_auth": {"username": credentials["username"], "password": credentials["password"]}} return None
for integration_id, (connection_name, key_field) in CONNECTIONS.items(): page = 0 while True: listed = requests.get( f"{NANGO_URL}/connections", headers=NANGO_HEADERS, params={"integrationId": integration_id, "limit": 1000, "page": page}, ) listed.raise_for_status() connections = listed.json()["connections"]
for connection in connections: identifier = (connection.get("tags") or {}).get("end_user_id") if not identifier: print("skipped, no end_user_id tag:", connection["connection_id"]) continue
full = requests.get( f"{NANGO_URL}/connections/{connection['connection_id']}", headers=NANGO_HEADERS, params={"provider_config_key": integration_id, "refresh_token": "true"}, ) full.raise_for_status() details = to_authorization_details(full.json()["credentials"], key_field) if details is None: print("skipped, credential type not mapped:", connection["connection_id"]) continue
try: created = actions.create_connected_account( connection_name=connection_name, identifier=identifier, authorization_details=details, ) print(connection_name, identifier, created.connected_account.status) except ScalekitServerException as e: print(connection_name, identifier, "failed:", e.error_code, e.message)
if len(connections) < 1000: break page += 1Save the code in a .mts file, such as migrate-nango.mts, and run it with npx tsx migrate-nango.mts. It needs Node.js 18 or later, for fetch.
import 'dotenv/config';import { ScalekitClient, ScalekitServerException, ConnectorStatus } from '@scalekit-sdk/node';import { timestampFromDate } from '@bufbuild/protobuf/wkt';
const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!,);const actions = scalekit.actions;
const NANGO_URL = 'https://api.nango.dev';const nangoHeaders = { Authorization: `Bearer ${process.env.NANGO_SECRET_KEY}` };
// Your Nango integration ID -> Scalekit connection name, and the field for an API keyconst CONNECTIONS: Record<string, { connectionName: string; keyField?: string }> = { 'google-mail': { connectionName: 'gmail' }, // OAuth openai: { connectionName: 'openai', keyField: 'token' }, // API key, sent as a bearer token twilio: { connectionName: 'twilio' }, // basic auth};
async function nango(path: string, params: Record<string, string>) { const res = await fetch(`${NANGO_URL}${path}?${new URLSearchParams(params)}`, { headers: nangoHeaders }); if (!res.ok) throw new Error(`Nango ${path}: ${res.status} ${await res.text()}`); return res.json();}
function toAuthorizationDetails(credentials: any, keyField?: string) { if (credentials.type === 'OAUTH2') { return { details: { case: 'oauthToken' as const, value: { accessToken: credentials.access_token, refreshToken: credentials.refresh_token, expiresAt: credentials.expires_at ? timestampFromDate(new Date(credentials.expires_at)) : undefined, }, }, }; } if (credentials.type === 'API_KEY' && keyField) { return { details: { case: 'staticAuth' as const, value: { details: { [keyField]: credentials.apiKey } } } }; } if (credentials.type === 'BASIC') { const fields = { username: credentials.username, password: credentials.password }; return { details: { case: 'staticAuth' as const, value: { details: fields } } }; } return undefined;}
for (const [integrationId, { connectionName, keyField }] of Object.entries(CONNECTIONS)) { for (let page = 0; ; page++) { const { connections } = await nango('/connections', { integrationId, limit: '1000', page: String(page), });
for (const connection of connections) { const identifier = connection.tags?.end_user_id; if (!identifier) { console.log('skipped, no end_user_id tag:', connection.connection_id); continue; }
const full = await nango(`/connections/${connection.connection_id}`, { provider_config_key: integrationId, refresh_token: 'true', }); const authorizationDetails = toAuthorizationDetails(full.credentials, keyField); if (!authorizationDetails) { console.log('skipped, credential type not mapped:', connection.connection_id); continue; }
try { const created = await actions.createConnectedAccount({ connectionName, identifier, authorizationDetails }); console.log(connectionName, identifier, created.connectedAccount && ConnectorStatus[created.connectedAccount.status]); } catch (err) { if (!(err instanceof ScalekitServerException)) throw err; console.log(connectionName, identifier, 'failed:', err.errorCode, err.message); } }
if (connections.length < 1000) break; }}Each imported account comes back ACTIVE. Change CONNECTIONS to your own integration IDs and connection names. For an API key, the field name depends on the Scalekit connector: OpenAI takes a bearer token, Exa takes api_key. See API keys, bearer tokens and basic auth for how to list a connector’s fields.
If a connection already exists for a user, for example because you ran the script twice, creating it again fails and the script prints the error. To replace the credential, use update_connected_account instead.
Move your agent code
Section titled “Move your agent code”Nango tools and actions are functions you wrote and deployed to Nango. In AgentKit, use the connector’s built-in tools where one does the same job, and move the rest into your app as custom tools that call the API through the Scalekit proxy.
Before (Nango, Node.js):
const result = await nango.callTool( { connectionId: '<CONNECTION-ID>', name: 'create-issue' }, { input: { owner: 'acme', repo: 'agent', title: 'Bug report' } },);After (Scalekit):
result = actions.execute_tool( tool_name="github_issue_create", identifier="user_123", connection_name="github", tool_input={"owner": "acme", "repo": "agent", "title": "Bug report"},)print(result.data)const result = await actions.executeTool({ toolName: 'github_issue_create', identifier: 'user_123', connector: 'github', toolInput: { owner: 'acme', repo: 'agent', title: 'Bug report' },});console.log(result.data);Nango addresses a user’s account by connectionId. AgentKit addresses it by your user’s identifier plus the connection name, so you don’t need to store a connection ID per user.
Before (Nango, Node.js):
const response = await nango.get({ endpoint: '/gmail/v1/users/me/messages', providerConfigKey: 'google-mail', connectionId: '<CONNECTION-ID>',});After (Scalekit):
response = actions.request( connection_name="gmail", identifier="user_123", method="GET", path="/gmail/v1/users/me/messages",)const response = await actions.request({ connectionName: 'gmail', identifier: 'user_123', method: 'GET', path: '/gmail/v1/users/me/messages',});If your agent used a Nango agent session’s MCP URL, create a Virtual MCP server with the same connections and tools. It has one static URL for all users; mint a session token for the user before each agent run and pass it as a bearer token.
API reference
The endpoints this page's code calls, with every field and the SDK method for each: