Link token migration guide 
===========================

#### How to migrate your application from the public key to a Link token 

Support for the public-key based integration mode ended on January 31, 2025. As of February 2025, public keys no longer work to launch Link sessions, and all integrations must use Link tokens.

Introduction 
-------------

Plaid has introduced a new `link_token`, which replaces the static `public_key`. This is an improvement that provides better error validation, more advanced security, and enables Link event logs to be surfaced in the Plaid Dashboard. As of February 2025, public keys no longer work to launch Link sessions, so all integrations must use a `link_token`.

This guide covers the client and server-side changes required to implement the new `link_token`. Here's an overview of the updates before we dive into the details.

**The Plaid flow** begins when your user wants to connect their bank account to your app.

(An image of "Step diagram")

**1**Call [/link/token/create](https://plaid.com/docs/api/link/index.html.md#linktokencreate) to create a `link_token` and pass the temporary token to your app's client.

(An image of "Step 1 diagram")

**2**Use the `link_token` to open Link for your user. In the [onSuccess callback](https://plaid.com/docs/link/web/index.html.md#onsuccess) , Link will provide a temporary `public_token`. This token can also be obtained on the backend via `/link/token/get`.

(An image of "Step 2 diagram")

**3**Call [/item/public\_token/exchange](https://plaid.com/docs/api/items/index.html.md#itempublic_tokenexchange) to exchange the `public_token` for a permanent `access_token` and `item_id` for the new `Item`.

(An image of "Step 3 diagram")

**4**Store the `access_token` and use it to make product requests for your user's `Item`.

(An image of "Step 4 diagram")

#### What's new 

*   Link will now be initialized with a new `link_token`. The `link_token` replaces all previous methods of initializing Link, including the `public_key` (for initial Link), the `public_token` (for update mode), and the `payment_token` (for Payment Initiation).
*   The new endpoint to create the `link_token` is [/link/token/create](https://plaid.com/docs/api/link/index.html.md#linktokencreate) .
*   The [INVALID\_LINK\_TOKEN](https://plaid.com/docs/errors/invalid-input/index.html.md#invalid_link_token) error code is now available to gracefully handle invalidated tokens.
*   Link events from sessions created with the new `link_token` will be surfaced in the [Logs](https://dashboard.plaid.com/activity/logs) section of the Dashboard. However, Link events from sessions created with the `public_key` will not.

#### Link tokens 

The `link_token` is a new type of token that is created by your app's server and passed to your app's client to initialize Link. The Link configuration parameters that were previously set within Link itself are now set via parameters passed to [/link/token/create](https://plaid.com/docs/api/link/index.html.md#linktokencreate) and conveyed to Link via the `link_token`. If these configurations are still set client-side when using the `link_token`, they will not have any effect on Link behavior.

Update your integration 
------------------------

The overall process for updating your integration is:

1.  Update your server to create a `link_token`.
2.  Update your client to pass the `link_token` and handle `INVALID_LINK_TOKEN` errors.
3.  Ensure you have updated all Link entry points, including those for update mode.
4.  Test your integration.
5.  Update any call sites that use the `public_key` for authentication to use the `client_id` and `secret` instead, then re-test those call sites.
6.  Disable the `public_key`.

Detailed instructions for each step can be found below.

#### Update your server 

Add a new authenticated endpoint to your app's server to create a `link_token` by calling [/link/token/create](https://plaid.com/docs/api/link/index.html.md#linktokencreate) .

```node
app.post('/api/create_link_token', async function (request, response) {
  // Get the client_user_id by searching for the current user
  const user = await User.find(...);
  const clientUserId = user.id;
  const linkTokenRequest = {
    user: {
      // This should correspond to a unique id for the current user.
      client_user_id: clientUserId,
    },
    client_name: 'Plaid Test App',
    products: ['transactions'],
    language: 'en',
    webhook: 'https://webhook.example.com',
    redirect_uri: 'https://domainname.com/oauth-page.html',
    country_codes: ['US'],
  };
  try {
    const createTokenResponse = await client.linkTokenCreate(linkTokenRequest);
    response.json(createTokenResponse.data);
  } catch (error) {
    // handle error
  }
});

```

```bash
curl -X POST https://sandbox.plaid.com/link/token/create \
-H 'Content-Type: application/json' \
-d '{
  "client_id": "${PLAID_CLIENT_ID}",
  "secret": "${PLAID_SECRET}",
  "client_name": "Plaid Test App",
  "user": { "client_user_id": "${UNIQUE_USER_ID}" },
  "products": ["transactions"],
  "country_codes": ["US"],
  "language": "en",
  "webhook": "https://webhook.example.com",
  "redirect_uri": "https://domainname.com/oauth-page.html"
}'

```

```ruby
post '/api/create_link_token' do
  # Get the client_user_id by searching for the current user
  current_user = User.find(...)
  client_user_id = current_user.id

  # Create a link_token for the given user
  request = Plaid::LinkTokenCreateRequest.new(
    {
      user: { client_user_id: client_user_id },
      client_name: 'Plaid Test App',
      products: ['transactions'],
      country_codes: ['US'],
      language: "en",
      redirect_uri: nil_if_empty_envvar('PLAID_REDIRECT_URI'),
      webhook: 'https://webhook.example.com'
    }
  )
  response = client.link_token_create(request)
  content_type :json
  response.to_json
end

```

```java
import com.plaid.client.model.Products;
import com.plaid.client.model.CountryCode;
import com.plaid.client.model.LinkTokenCreateRequest;
import com.plaid.client.model.LinkTokenCreateRequestUser;
import com.plaid.client.model.LinkTokenCreateResponse;

public class PlaidExample {

  ...
  static class GetLinkToken implements HttpHandler {
    private static PlaidApi plaidClient;

    public void handle(HttpExchange t) throws IOException {
      // Create your Plaid client
      HashMap apiKeys = new HashMap();
      apiKeys.put("clientId", CLIENT_ID);
      apiKeys.put("secret", SECRET);
      ApiClient apiClient = new ApiClient(apiKeys);
      apiClient.setPlaidAdapter(ApiClient.Sandbox);

      plaidClient = apiClient.createService(PlaidApi.class);

      // Get the clientUserId by searching for the current user
      User userFromDB = db.find(...);
      String clientUserId = userFromDB.id;
      LinkTokenCreateRequestUser user = new LinkTokenCreateRequestUser()
        .clientUserId(clientUserId);

      // Create a link_token for the given user
      LinkTokenCreateRequest request = new LinkTokenCreateRequest()
        .user(user)
        .clientName("Plaid Test App")
        .products(Arrays.asList(Products.fromValue("transactions")))
        .countryCodes(Arrays.asList(CountryCode.US))
        .language("en")
        .redirectUri("https://domainname.com/oauth-page.html")
        .webhook("https://webhook.example.com");

      Response response = plaidClient
        .linkTokenCreate(request)
        .execute();

      // Send the data to the client
      return response.body();
    }
  }
}

```

```python
from plaid.model.link_token_create_request import LinkTokenCreateRequest
from plaid.model.link_token_create_request_user import LinkTokenCreateRequestUser
from plaid.model.products import Products
from plaid.model.country_code import CountryCode

@app.route("/create_link_token", methods=['POST'])
def create_link_token():
    # Get the client_user_id by searching for the current user
    user = User.find(...)
    client_user_id = user.id

    # Create a link_token for the given user
    request = LinkTokenCreateRequest(
            products=[Products("transactions")],
            client_name="Plaid Test App",
            country_codes=[CountryCode('US')],
            redirect_uri='https://domainname.com/oauth-page.html',
            language='en',
            webhook='https://webhook.example.com',
            user=LinkTokenCreateRequestUser(
                client_user_id=client_user_id
            )
        )
    response = client.link_token_create(request)

    # Send the data to the client
    return jsonify(response.to_dict())


```

```go
func createLinkToken(c *gin.Context) {
  ctx := context.Background()

  // Get the client_user_id by searching for the current user
  user, _ := usermodels.Find(...)
  clientUserId := user.ID.String()

  // Create a link_token for the given user
  request := plaid.NewLinkTokenCreateRequest("Plaid Test App", "en", []plaid.CountryCode{plaid.COUNTRYCODE_US}, *plaid.NewLinkTokenCreateRequestUser(clientUserId))
  request.SetWebhook("https://webhook.sample.com")
  request.SetRedirectUri("https://domainname.com/oauth-page.html")
  request.SetProducts([]plaid.Products{plaid.PRODUCTS_TRANSACTIONS})

  resp, _, err := testClient.PlaidApi.LinkTokenCreate(ctx).LinkTokenCreateRequest(*request).Execute()

  // Send the data to the client
  c.JSON(http.StatusOK, gin.H{
    "link_token": resp.GetLinkToken(),
  })
}


```

Many of the parameters to [/link/token/create](https://plaid.com/docs/api/link/index.html.md#linktokencreate) are the same as parameters previously set in Link. Aside from the change to snake case from camelCase, there are a few substantive differences, summarized below. Note that these bullets are only a summary; for the full [/link/token/create](https://plaid.com/docs/api/link/index.html.md#linktokencreate) endpoint definition, see the [API Reference](https://plaid.com/docs/api/link/index.html.md#linktokencreate) .

*   A new required parameter, `user.id`, has been added. This should be a unique identifier, such as the user ID of the end user in your application. It should not contain personally identifiable information, such as a phone number or email address.
*   `language` and `country_codes`, which were previously optional, are now required.
*   `accountSubtypes` has been replaced by the `account_filters` parameter, and its syntax has changed.

In addition, there are a few differences relevant specifically to European integrations:

*   The `oauthNonce` parameter is no longer used, since it is effectively replaced by `user.id`.
*   For the Payment Initiation product, the `paymentToken` is no longer used, and `/payment_initiation/payment/token/create` has been deprecated. Instead, the `payment_id` should be provided to [/link/token/create](https://plaid.com/docs/api/link/index.html.md#linktokencreate) via the `payment_initiation.payment_id` parameter. Initializing Link with the returned `link_token` will launch the Payment Initiation flow.

##### Authenticate your app 

The endpoint used to create a `link_token` should only be available to users that are logged in to your app. Once your user is logged in, pass an identifier that uniquely identifies your user into the `user.client_user_id` field. The value of this field should not be personally identifiable information such as an email address or phone number. Using `user.client_user_id` will allow for easier debugging in the [Dashboard logs](https://dashboard.plaid.com/activity/logs) . You will be able to search for Link logs that belong to one of your end users.

As this update involves an additional API call when adding an Item, create a `link_token` when your user initially visits your app to avoid adding latency to your Link flow.

#### Update your client 

For each of your web and mobile apps, use the new endpoint you created to fetch a `link_token`, then pass it into one of Plaid's Link SDKs to initialize Link. You can then safely remove the `public_key` and other client-side configs that are now configured in the [/link/token/create](https://plaid.com/docs/api/link/index.html.md#linktokencreate) request.

If the token expires or the user enters too many invalid credentials, the `link_token` can become invalidated. If it does get into an invalid state, Link will exit with an [INVALID\_LINK\_TOKEN](https://plaid.com/docs/errors/invalid-input/index.html.md#invalid_link_token) error code. By recognizing when this error occurs in the `onExit` callback, you can generate a fresh `link_token` for the next time your user opens Link.

##### Update Link web 

The code below demonstrates code that passes the new `link_token` to Link. For more in-depth coverage on how to integrate with Link web, see the [Link web docs](https://plaid.com/docs/link/web/index.html.md) .

Note that in the error handling section, in order to handle an invalid `link_token` for Link in the browser, you will need to gracefully clean up the old iframe before reinitializing Link. To do this, use the `destroy()` method and reinitialize Link with a new `link_token` in the `onExit` callback.

Initialize Link with a Link Token (Web)

```javascript
<button id="link-button">Link Account</button>
<script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
<script type="text/javascript">
(async function() {
  const fetchLinkToken = async () => {
    const response = await fetch('/create_link_token', { method: 'POST' });
    const responseJSON = await response.json();
    return responseJSON.link_token;
  };

  const configs = {
    // 1. Pass a new link_token to Link.
    token: await fetchLinkToken(),
    onSuccess: async function(public_token, metadata) {
      // 2a. Send the public_token to your app server.
      // The onSuccess function is called when the user has successfully
      // authenticated and selected an account to use.
      await fetch('/exchange_public_token', {
        method: 'POST',
        body: JSON.stringify({ public_token: public_token }),
      });
    },
    onExit: async function(err, metadata) {
      // 2b. Gracefully handle the invalid link token error. A link token
      // can become invalidated if it expires, has already been used
      // for a link session, or is associated with too many invalid logins.
      if (err != null && err.error_code === 'INVALID_LINK_TOKEN') {
        linkHandler.destroy();
        linkHandler = Plaid.create({
          ...configs,
          token: await fetchLinkToken(),
        });
      }
      if (err != null) {
        // Handle any other types of errors.
      }
      // metadata contains information about the institution that the
      // user selected and the most recent API request IDs.
      // Storing this information can be helpful for support.
    },
  };

  var linkHandler = Plaid.create(configs);

  document.getElementById('link-button').onclick = function() {
    linkHandler.open();
  };
})();
</script>
```

##### Update Link iOS 

The iOS SDK now provides an `initWithLinkToken` method on both the `PLKConfiguration` and the `PLKPlaidLinkViewController` classes that should allow you to easily initialize Link with a `link_token`.

The code below shows how to initialize Link with the `link_token` in iOS. For more in-depth coverage on how to integrate with Link iOS, see the [iOS docs](https://plaid.com/docs/link/ios/index.html.md) .

```swift
let linkConfiguration = PLKConfiguration(linkToken: "GENERATED_LINK_TOKEN")
let linkViewDelegate = self
let linkViewController = PLKPlaidLinkViewController(
  linkToken: "GENERATED_LINK_TOKEN",
  configuration: linkConfiguration,
  delegate: linkViewDelegate,
)
if (UI_USER_INTERFACE_IDIOM() == .pad) {
    linkViewController.modalPresentationStyle = .formSheet;
}
present(linkViewController, animated: true)

```

```objectivec
PLKConfiguration* linkConfiguration;
@try {
  linkConfiguration = [[PLKConfiguration alloc] initWithLinkToken:@"GENERATED_LINK_TOKEN"];
  id linkViewDelegate  = self;
  PLKPlaidLinkViewController* linkViewController = [[PLKPlaidLinkViewController alloc] initWithLinkToken:@"GENERATED_LINK_TOKEN" configuration:linkConfiguration delegate:linkViewDelegate];
  if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
      linkViewController.modalPresentationStyle = UIModalPresentationFormSheet;
  }
  [self presentViewController:linkViewController animated:YES completion:nil];
} @catch (NSException *exception) {
  NSLog(@"Invalid configuration: %@", exception);
}

```

##### Update Link Android 

The Android SDK exposes a new class called `LinkTokenConfiguration`. This class accepts the `link_token` and should be passed into the `openPlaidLink` method.

The code below demonstrates how to use the `LinkTokenConfiguration` class to open Link. For more in depth coverage on the Android SDK, see the [Android docs](https://plaid.com/docs/link/android/index.html.md) .

```kotlin
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

import com.plaid.link.Plaid
import com.plaid.link.linkTokenConfiguration
import com.plaid.link.openPlaidLink
import com.plaid.link.configuration.AccountSubtype
import com.plaid.link.configuration.LinkLogLevel
import com.plaid.link.configuration.PlaidEnvironment
import com.plaid.link.configuration.PlaidProduct
import com.plaid.link.event.LinkEvent
import java.util.Locale

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Optional
    Plaid.setLinkEventListener { event -> Log.i("Event", event.toString()) }

    // Open Link – put this inside of a Button / Fab click listener
    this@MainActivity.openPlaidLink(
      linkTokenConfiguration {
        // required
        token = "GENERATED_LINK_TOKEN"

        // optional
        logLevel = LinkLogLevel.WARN // Defaults to ASSERT
        extraParams = mapOf() // Map of additional configs
      }
    );
  }
}

```

```java
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;

import com.plaid.link.Plaid;
import com.plaid.link.openPlaidLink;
import com.plaid.link.configuration.AccountSubtype;
import com.plaid.link.configuration.LinkTokenConfiguration;
import com.plaid.link.configuration.LinkLogLevel;
import com.plaid.link.configuration.PlaidEnvironment;
import com.plaid.link.configuration.PlaidProduct;
import com.plaid.link.event.LinkEvent;
import java.util.Locale;
import kotlin.Unit;

public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Optional
    Plaid.setLinkEventListener(linkEvent -> {
      Log.i("Event", linkEvent.toString());
      return Unit.INSTANCE;
    });

    // Open Link – put this inside of a Button / Fab click listener
    Plaid.openLink(
        this,
        new LinkTokenConfiguration.Builder()
            .token("GENERATED_LINK_TOKEN")
            .logLevel(LinkLogLevel.WARN) // Defaults to ASSERT
            .build()
            .toLinkConfiguration()
    );
  }
}

```

#### Update Link update mode flows 

With the introduction of the `link_token`, `/item/public_token/create` is deprecated, and Link's update mode is initialized by passing in a `link_token` rather than a `public_token`. You can obtain this `link_token` by calling [/link/token/create](https://plaid.com/docs/api/link/index.html.md#linktokencreate) and providing the `user.id` of the user whose Item is being updated, along with the `access_token` for the Item. Make sure to update any update mode flow entry points in addition to updating primary Link flows. For more details and complete sample code, see [Updating Items via Link](https://plaid.com/docs/link/update-mode/index.html.md) .

Initializing Link with a link\_token for update mode

```javascript
// Initialize Link with the token parameter
// set to the generated link_token for the Item
const linkHandler = Plaid.create({
  token: 'GENERATED_LINK_TOKEN',
  onSuccess: (public_token, metadata) => {
    // You do not need to repeat the /item/public_token/exchange
    // process when a user uses Link in update mode.
    // The Item's access_token has not changed.
  },
  // ...
});
```

#### Test in Sandbox 

Once you have updated both your app's client and server, it's time to test that your integration works. The best way to test is by using the test credentials in the Sandbox:

```json
username: user_good
password: pass_good
```

Test your error handling flow for `INVALID_LINK_TOKEN` by using the Sandbox test credentials to force an error:

```json
username: user_custom
password: { "force_error": "INVALID_LINK_TOKEN" }
```

You can also verify that you have updated correctly by viewing Link event logs in the [Plaid Dashboard](https://dashboard.plaid.com/activity/logs) .

To test your update mode implementation, use the [/sandbox/item/reset\_login](https://plaid.com/docs/api/sandbox/index.html.md#sandboxitemreset_login) endpoint to force an Item into a state that requires an update, then walk through the test steps above.

#### Update API endpoints 

In order to completely migrate off of the `public_key`, there are a few Plaid API endpoints that should replace the `public_key` with the `client_id` and `secret`: [/institutions/search](https://plaid.com/docs/api/institutions/index.html.md#institutionssearch) , [/institutions/get\_by\_id](https://plaid.com/docs/api/institutions/index.html.md#institutionsget_by_id) , and [/sandbox/public\_token/create](https://plaid.com/docs/api/sandbox/index.html.md#sandboxpublic_tokencreate) .

Because the `client_id` and `secret` are now used to authenticate the endpoints above, they should only be called from your server. The ability to call them from the client has been removed in the latest client library updates.

#### Disable the public key 

After completing all of the above steps, you can now confidently disable the `public_key` via the [Plaid Dashboard](https://dashboard.plaid.com/developers/keys) . This can be done on a per-environment basis, with different settings for Sandbox and Production, to help you test your migration. The `public_key` can also be disabled separately for Link and for the API. Disabling it for Link means you will be required to use the `link_token` to initialize Link. Disabling it for the API means that you will be required to use the `client_id` and `secret` to call [/institutions/search](https://plaid.com/docs/api/institutions/index.html.md#institutionssearch) , [/institutions/get\_by\_id](https://plaid.com/docs/api/institutions/index.html.md#institutionsget_by_id) , and[/sandbox/public\_token/create](https://plaid.com/docs/api/sandbox/index.html.md#sandboxpublic_tokencreate) .

(An image of "Dashboard UI for disabling the public key. Options to disable for Link and API in Sandbox, Development, and Production. Save changes button present.")

It is recommended you disable the `public_key` as soon as possible to ensure that you do not accidentally add legacy `public_key`\-dependent code to your application.

#### Conclusion 

Congratulations on upgrading to the new `link_token`! If you require any help migrating to the `link_token` integration, please [contact Plaid support](https://dashboard.plaid.com/support/new/product-and-development/developer-lifecycle/link) for assistance.