> **Building with AI coding agents?** Install the authstack plugin with one command. This equips your agent with accurate Scalekit implementation patterns.
>
> **Recommended** (the `-y` flags skip prompts, so the command runs without an interactive terminal):
> ```bash
> npx -y @scalekit-inc/cli setup -y
> ```
>
> Global:
> ```bash
> npm install -g @scalekit-inc/cli
> scalekit setup -y
> ```
>
> Supports Claude Code, Cursor, GitHub Copilot, Codex + skills for other Agent Skills-compatible agents in `~/.agents/skills`.
> Skills: integrate-agentkit, implement-saaskit, add-mcp-oauth, implement-sso, implement-scim.
> [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# 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](/agentkit/advanced/migrate-to-agentkit/).

## How Nango maps to AgentKit

| Nango | AgentKit |
|---|---|
| Integration (integration ID, `provider_config_key`) | [Connection](/agentkit/connections/), referred to by its connection name |
| Connection (`connection_id`) | [Connected account](/agentkit/connected-accounts/): one per user and connection |
| `end_user_id` tag | [Identifier](/agentkit/concepts/): your app's ID for the user, passed on each call |
| Connect UI and connect session | [Authorization link](/agentkit/tools/authorize/) |
| Tools and actions you write | [Connector tools](/agentkit/connectors/), or [custom tools](/agentkit/tools/custom-tools/) that call the API through the proxy |
| Proxy (`nango.proxy()`, `nango.get()`) | [`actions.request()`](/agentkit/tools/custom-tools/) through the Scalekit proxy |
| Agent sessions over MCP | [Virtual MCP server](/agentkit/mcp/overview/) with a session token per agent run |

## 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](/agentkit/advanced/bring-your-own-oauth/). 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](/agentkit/advanced/migrate-to-agentkit/#option-a-users-authorize-again).
- **API keys and basic auth:** always.
- **Your Nango secret key** has the `environment:connections:read_credentials` scope. 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](/agentkit/advanced/migrate-to-agentkit/#1-set-up-scalekit):

```bash title=".env"
NANGO_SECRET_KEY=your_nango_secret_key
```

  **Python**

  Install `requests` too: `pip install scalekit-sdk-python python-dotenv requests`. The script needs Python 3.11 or later.

  ```python
  import os
  from datetime import datetime

  import requests
  from dotenv import load_dotenv
  from scalekit import ScalekitClient
  from 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 += 1
  ```

  **Node.js**

  Save 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`.

  ```typescript
  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 key
  const 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](/agentkit/advanced/migrate-to-agentkit/#api-keys-bearer-tokens-and-basic-auth) for how to list a connector's fields.

> caution
>
> The script handles each user's credentials. Run it on your server, don't log the credentials, and remove the Nango connections once your agent runs on Scalekit.

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

### Tools

Nango tools and actions are functions you wrote and deployed to Nango. In AgentKit, use the [connector's built-in tools](/agentkit/connectors/) where one does the same job, and move the rest into your app as [custom tools](/agentkit/tools/custom-tools/) that call the API through the Scalekit proxy.

**Before (Nango, Node.js):**
```typescript
const result = await nango.callTool(
  { connectionId: '<CONNECTION-ID>', name: 'create-issue' },
  { input: { owner: 'acme', repo: 'agent', title: 'Bug report' } },
);
```

**After (Scalekit):**

  **Python**

  ```python
  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)
  ```

  **Node.js**

  ```typescript
  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.

### Proxy

**Before (Nango, Node.js):**
```typescript
const response = await nango.get({
  endpoint: '/gmail/v1/users/me/messages',
  providerConfigKey: 'google-mail',
  connectionId: '<CONNECTION-ID>',
});
```

**After (Scalekit):**

  **Python**

  ```python
  response = actions.request(
      connection_name="gmail",
      identifier="user_123",
      method="GET",
      path="/gmail/v1/users/me/messages",
  )
  ```

  **Node.js**

  ```typescript
  const response = await actions.request({
    connectionName: 'gmail',
    identifier: 'user_123',
    method: 'GET',
    path: '/gmail/v1/users/me/messages',
  });
  ```

### MCP

If your agent used a Nango agent session's MCP URL, create a [Virtual MCP server](/agentkit/mcp/overview/) 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.

## Next

  - [Migrate to AgentKit](/agentkit/advanced/migrate-to-agentkit/): The steps every migration shares, including the import fields.
  - [Custom tools](/agentkit/tools/custom-tools/): Move your own Nango functions into your app.

## API reference

The endpoints this page's code calls, with every field and the SDK method for each:

- `POST` [Create a connected account](https://docs.scalekit.com/agentkit/reference/connected-accounts/create-a-connected-account.md)
- `POST` [Execute a tool](https://docs.scalekit.com/agentkit/reference/tools/execute-a-tool.md)


---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
