How to Setup and Configure Plaid in PHP or Laravel

TLDR – If you don’t care about following along with the guide, you can find all the code here. Though I’d recommend reading below to see how everything works together.


As of this writing, Plaid has SDKs for Node, Python, Ruby, Java, and Go, but no such (official) library exists for PHP. In my opinion, that’s pretty disappointing from a company as large as Plaid, but that’s a topic for another day.

I’ve now used Plaid in two different FinTech SaaS applications built in Laravel, and I’ve come to find that there’s virtually no documentation out there on how to get things linked up and set up properly using PHP or Laravel. My goal with this article is to show you how to quickly and easily get things up and running for your application.

In this example, I’m going to be using a blank Laravel install (and I’m going to assume that’s what you’re using as well). If you’re not using Laravel, however, this should translate over to PHP, Symfony, or any other framework you’re using easily.

Assumptions

For this tutorial, I’m assuming you have the following:

  • Composer installed (if you don’t, you can do so here: https://getcomposer.org/download/)
  • A blank Laravel installation ready to go. I’m using v9.37, but any modern version should work just fine
  • A MySQL  instance available and connected to your Laravel install
  • PHP 8.0+
  • Npm/node
  • You’ve already created a Plaid account and at least have Sandbox access/keys

Laravel Welcome Screen

Install TomorrowIdeas Library

Although Plaid doesn’t have an official SDK, there’s a pretty good community-supported SDK built and maintained by TomorrowIdeas which we’ll be using. You can read more about that here: https://github.com/TomorrowIdeas/plaid-sdk-php

Let’s go ahead and install it:

composer require tomorrow-ideas/plaid-sdk-php

Modify Welcome File

First things first, we’re going to modify the welcome file located in /resources/views/welcome.blade.php, clear out the file, and replace it with this. Also, I’m going to be using JQuery in this example, which may make some of you cringe, but bite me. Ha!

This code should be pretty straightforward for most people. It’s a blank HTML page with a button, and then also includes the Plaid libraries, as well as Jquery. Other than that, it’s nothing fancy.

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}" />

        <title>Plaid Test</title>

        <!-- Fonts -->
        <link href="https://fonts.bunny.net/css2?family=Nunito:[email protected];600;700&display=swap" rel="stylesheet">

        <!-- Styles -->
        <style>
            body {
                font-family: 'Nunito', sans-serif;
            }

            #wrapper {
                width: 100%;
                height: 400px;
            }

            button {
                height: 20px;
                position: relative;
                margin: -20px -50px;
                width: 100px;
                top: 50%;
                left: 50%;
            }

        </style>

    <!-- Scripts -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
    <script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
    @vite(['resources/css/app.css', 'resources/js/app.js'])

    </head>
    <body>
        <div id="wrapper">
            <button type="button" class="link-account">Link Account</button>
        </div>
    </body>
</html>

It should look something like so:

Add Plaid Keys to .env File

Let’s open up your .env file and add your Plaid keys. You can find these in your Plaid account under “Team Settings” and then “Keys”. Obviously, you’ll want to update your base url and webhook URL to your own site.

PLAID_BASE_URL=https://plaid.test
PLAID_WEBHOOK=https://plaid.test
PLAID_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXX
PLAID_SECRET=XXXXXXXXXXXXXXXXXXX
PLAID_ENV=sandbox

Create Link Token Route

Now, let’s get to the fun stuff. We need to actually make our button clickable. First thing we need to do is add a route, which will redirect to an entry inside of our controller. Add this to your /routes/web.php file. Keep in mind that I’ve named my controller ‘PlaidController’, but if yours is named something else (which it likely is), then you’ll want to modify the command below accordingly.

Route::get('/createLinkToken', 'App\Http\Controllers\[email protected]');

Create Controller and Add Logic

Inside of our Controller, this is where we request some data from Plaid. First, let’s create a controller that matches the name of whatever we assigned in the previous step. In this case, it’s going to be PlaidController.

php artisan make:controller PlaidController

You should now have a new file created in app/Http/Controllers/PlaidController.php. Let’s navigate to it now.

Inside of the PlaidController.php file, add the following:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use TomorrowIdeas\Plaid\Entities\User;
use TomorrowIdeas\Plaid\Plaid;
use Illuminate\Support\Facades\Log;
use App\Models\PlaidAccounts;
use App\Models\Holdings;

class PlaidController extends Controller
{
    public function createLinkToken()
    {
        Log::info('-----------------------------------------');
        Log::info('Creating new link token');
        $user_id = 1;
        $plaidUser = new User($user_id);
        $plaid = new Plaid(env('PLAID_CLIENT_ID'), env('PLAID_SECRET'), env('PLAID_ENV'));
        $response = $plaid->tokens->create('Plaid Test', 'en', ['US'], $plaidUser, ['investments'], env('PLAID_WEBHOOK'));
        Log::info('Plaid link_token - User: ' . $user_id . ', ' . json_encode($response));
        return response()->json([
            'result' => 'success',
            'data' => json_encode($response)
        ], 200);
    }

There’s a lot going on in this file, so let’s discuss what’s happening.

First things first, we’re going to log everything to our laravel.log file. Whenever a new link token is created, we’re going to output that to the log file.

We’re then going to get the user’s ID and use it to pass to Plaid. For the purpose of this tutorial, I’m hard-coding this value, but in a typical authenticated application, you’ll want this to be dynamic, so you’d do something along the lines of $plaidUser = new User($user->id);

We then pull our Client ID, Secret ID, and Plaid Environment from the .env file that we modified earlier, and then pass that into Plaid to get a link token.

One thing to mention is that I’m only requesting US data from Plaid. If you want, you could enter US, CAD, or whatever other currencies/regions you want to support. Also, I’m only going to be pulling Investment holding data, but you could modify the above array to be ['transactions','auth'] if you want to pull transactions, auth, or any variation/combination.

We then send all that data to Plaid, and if we do everything correctly, we’ll get a response.

Before this will work, though, we need to set up our button so that it fires this controller when clicked.

Making our Button Clickable

Open up your /resources/js/app.js file, and paste the following:

$(".link-account").click(function() {
    createLinkToken();
});

function createLinkToken() {
    $.ajax({
        headers: {
            "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content")
        },
        url: "/createLinkToken",
        type: "GET",
        dataType: "json",
        success: function (response) {
            const data = JSON.parse(response.data);
            console.log('Link Token: ' + data.link_token);
            linkPlaidAccount(data.link_token);
        },
        error: function (err) {
            console.log('Error creating link token.');
            const errMsg = JSON.parse(err);
            alert(err.error_message);
            console.error("Error creating link token: ", err);
        }
    });
}

In your console, you’ll then need to run the following command:

npm run build

Note: If you’re using Laravel version 8 or older, you’ll want to run npm run prod instead, since that triggers Laravel Mix. Version 9+ has switched over to Vite as the builder tool to compile CSS/JS assets.

Now, if you refresh your browser and click on your “Link Account” button, assuming everything is configured correctly, you should be able to see a link token by opening up the Chrome Developer Tools and heading over to the network tab.

You’ll also notice that we’re logging to our laravel.log file now, and you can see the results of requesting a link token. This is good to have access to, because if you run into issues and have to open a ticket with Plaid, they’ll request either the link_token or the request_id, and you can easily pull it from here.laravel.log file

Initializing Plaid

Next, let’s add the code that actually opens up the Plaid window. This function will fire as soon as you click on the button. If after adding this code the window doesn’t appear, make sure that you check two things: 1) Make sure you re-run the build process and 2) make sure that you included the plaid javascript script file in our welcome file.

function linkPlaidAccount(linkToken) {
    var linkHandler = Plaid.create({
        token: linkToken,
        onSuccess: function (public_token, metadata) {
            var body = {
                public_token: public_token,
                accounts: metadata.accounts,
                institution: metadata.institution,
                link_session_id: metadata.link_session_id,
                link_token: linkToken
            };
            $.ajax({
                headers: {
                    "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content")
                },
                url: "/storePlaidAccount",
                type: "POST",
                data: body,
                dataType: "json",
                success: function (data) {
                    getInvestmentHoldings(data.item_id);
                },
                error: function (err) {
                    console.log('Error linking Plaid account.');
                    const errMsg = JSON.parse(err);
                    console.error("Error linking Plaid account: ", err);
                }
            });
        },
        onExit: function (err, metadata) {
            console.log("linkBankAccount error=", err, metadata);
            const errMsg = JSON.parse(err);
                    console.error("Error linking Plaid account: ", err);

            linkHandler.destroy();
            if (metadata.link_session_id == null && metadata.status == "requires_credentials") {
                createLinkToken();
            }
        }
    });
    linkHandler.open();
}

Again, there’s a lot going on in this file, so let’s talk about it.

First things first, this will open the Plaid window. Once you walk through the steps within the Plaid Link window and get a “Successful” connection message, we’ll get a bunch of data back from Plaid, and we’ll need to know how to handle that.

That’s when the “storePlaidAccount” url is triggered, which, right now, doesn’t exist. Let’s add it to our web.php routes file.

Route::post('/storePlaidAccount', 'App\Http\Controllers\[email protected]');

Next, we also need to add some code inside of our PlaidController.php controller so that routes file can have a successful endpoint. Add this to the end of your PlaidController.php file:

    public function storePlaidAccount(Request $request)
    {
        $validator = \Validator::make($request->all(), [
            'public_token' => ['required', 'string']
        ]);

        if ($validator->fails()) {
            return response()->json(['result' => 'error', 'message' => $validator->errors()], 201);
        }

        $user_id = 1;
        Log::info('-----------------------------------------');
        Log::info('Plaid public_token : ' . $request->public_token . ', link_token: ' . $request->link_token);
        $plaid = new Plaid(env('PLAID_CLIENT_ID'), env('PLAID_SECRET'), env('PLAID_ENV'));
        $obj = $plaid->items->exchangeToken($request->public_token);
        Log::info('Plaid exchange token : ' . json_encode($obj));

        try {
            \DB::transaction(function () use($request, $obj, $user_id) {
                foreach($request->accounts as $account) {
                    $query = PlaidAccounts::where('account_id', isset($account['id']) ? $account['id'] : $account['account_id']);
                    if ($query->count() > 0) {
                        Log::info('[Update Plaid Account]: ' . json_encode($account));
                        $new_account = $query->first();
                        $new_account->plaid_item_id = $obj->item_id;
                        $new_account->plaid_access_token = $obj->access_token;
                        $new_account->plaid_public_token = $request->public_token;
                        $new_account->link_session_id = $request->link_session_id;
                        $new_account->link_token = $request->link_token;
                        $new_account->institution_id = $request->institution['institution_id'];
                        $new_account->institution_name = $request->institution['name'];
                        $new_account->account_id = isset($account['id']) ? $account['id'] : $account['account_id'];
                        $new_account->account_name = isset($account['name']) ? $account['name'] : $account['account_name'];
                        $new_account->account_mask = isset($account['account_number']) ? $account['account_number'] : $account['mask'];
                        $new_account->account_mask = null;
                        $new_account->account_type = isset($account['type']) ? $account['type'] : $account['account_type'];
                        $new_account->account_subtype = isset($account['subtype']) ? $account['subtype'] : $account['account_sub_type'];
                        $new_account->user_id = $user_id;
                        $new_account->save();
                    } else {
                        Log::info('[New Plaid Account]: ' . json_encode($account));
                        $new_account = ([
                            'plaid_item_id' => $obj->item_id,
                            'plaid_access_token' => $obj->access_token,
                            'plaid_public_token' => $request->public_token,
                            'link_session_id' => $request->link_session_id,
                            'link_token' => $request->link_token,
                            'institution_id'    => $request->institution['institution_id'],
                            'institution_name' => $request->institution['name'],
                            'account_id' => isset($account['id']) ? $account['id'] : $account['account_id'],
                            'account_name' => isset($account['name']) ? $account['name'] : $account['account_name'],
                            'account_mask' => isset($account['account_number']) ? $account['account_number'] : $account['mask'],
                            'account_mask' => null,
                            'account_type' => isset($account['type']) ? $account['type'] : $account['account_type'],
                            'account_subtype' => isset($account['subtype']) ? $account['subtype'] : $account['account_sub_type'],
                            'user_id' => $user_id
                        ]);
                        PlaidAccounts::create($new_account);
                    }
                }
            });
        } catch (\Exception $e) {
            Log::error('An error occurred linking a Plaid account: ' . $e->getMessage());
            return response()->json([
                'message' => 'An error occurred attempting to link a Plaid account.'
            ], 200);
        }
        return response()->json([
            'message' => 'Successfully linked plaid account.',
            'item_id' => $obj->item_id
        ], 200);
    }

Essentially, what this ^^^ code does is it stores a bunch of data from Plaid regarding the account that you just linked. If that account already exists in the database, it’ll update it rather than creating a duplicate. We store a bunch of things, such as the item_id, access_token, institution id, institution name, and more. You can add or remove as much of this as you want. None of it is required to keep, except for the plaid_access_token, which we’ll need to use in just a minute.

Create Database Migration to Store Plaid Account Data

Okay, cool. All this is great, but it still won’t work until we create a database migration. More than just a migration, though, let’s also create a model and controller so we can easily add things from a Model endpoint. Let’s do that now.

php artisan make:model PlaidAccounts -mcr

Note: The -mcr flag creates a migration, and a controller with resources in one fell swoop. Cool.

Open up the newly-created migration (it’ll be under your “Database” and then “Migrations” folder), and add the following:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePlaidAccountsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('plaid_accounts', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('plaid_item_id');
            $table->string('plaid_access_token');
            $table->string('plaid_public_token');
            $table->string('link_session_id');
            $table->string('link_token');
            $table->string('institution_id');
            $table->string('institution_name');
            $table->string('account_id');
            $table->string('account_name');
            $table->string('account_mask')->nullable();
            $table->string('account_type');
            $table->string('account_subtype');
            $table->unsignedBigInteger('user_id')->nullable();
            $table->datetime('last_update')->nullable();
            $table->string('last_status')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('plaid_accounts');
    }
}

Perfect. Now, we just need to run the migration:

php artisan migrate

If you do everything correctly, you should now be able to go through a full Plaid flow successfully, and you should see an entry in the database now, storing our Plaid account information.

Note: Make sure you use “user_good” as the username and “pass_good” as the password when you’re in Sandbox mode.

Note 2: Because we selected the “Investments” product in our code earlier, you’ll need to select an account that’s an “Investment” account, otherwise you’ll get an error message. As you can see in the screenshots below, I’ve selected the 401k, since it’s a valid investment account. You won’t be able to select a Checking or Savings account, or the link flow won’t complete.

Plaid Flow 1

Plaid Flow 2

Plaid Flow 3

Plaid Flow 4

Plaid Flow 5

Plaid Successful Account Link

 

If you see an entry in the database like this ^^^, great job! Everything is looking great.

Importing Investment Holdings from Plaid

Great! So we’ve got an account in the database from Plaid now, but there’s more that we want to do. What if we want to pull a user’s actual stock positions within their 401k? We can absolutely do that. First, let’s great a new migration to hold the data.

php artisan make:migration create_holdings_table

And let’s add the following to that migration:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateHoldingsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('holdings', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('holding_id')->nullable(true);
            $table->integer('user_id')->nullable(false);
            $table->decimal('cost_basis',15,6)->nullable(false);
            $table->decimal('price',15,2)->nullable(false);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('holdings');
    }
}

We also need to add another entry to our web.php routes file.

Route::post('/getInvestmentHoldings', 'App\Http\Controllers\[email protected]');

Let’s add this to the bottom of our app.js file. Once we have a successful Plaid link flow, and the account is added to our database, it’ll take the newly-created account and pull the investment holdings and store them in our database.

function getInvestmentHoldings(itemId) {
    var body = {
        itemId: itemId,
    };
    $.ajax({
        headers: {
            "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content")
        },
        url: "/getInvestmentHoldings",
        type: "POST",
        data: body,
        dataType: "json",
        success: function (data) {
            console.log("Plaid holdings successfully imported.");
        },
        error: function (err) {
            const errMsg = JSON.parse(err);
            alert(err.error_message);
            console.error("Error importing holdings from Plaid: ", err);
        }
    });
}

Don’t forget to re-run our build process, after modifying our Javascript.

npm run build

And lastly, we need to add the following code to our controller so that we can store the holdings that we receive from Plaid:

    public function getInvestmentHoldings(Request $request) {
        if ($request->itemId != NULL) {
            $account = PlaidAccounts::where('plaid_item_id', $request->itemId)->first();
            Log::info('Account pulled: ' . $account);
        }

        if (!isset($plaid)) {
            $plaid = new Plaid(env('PLAID_CLIENT_ID'), env('PLAID_SECRET'), env('PLAID_ENV'));
        }


        try {
            \DB::transaction(function () use ($plaid, $account) {
                try {
                    $results = $plaid->investments->listHoldings($account->plaid_access_token);
                    $account->last_update = new \DateTime();
                    $account->last_status = '';
                    $account->save();
                } catch (\Exception $e) {
                    $response = json_decode(json_encode($e->getResponse(), true), true);
                    $account->last_status = $response['error_code'];
                    $account->save();
                    Log::error('Error pulling holdings from Plaid: '.$response['error_code']);
                    return response()->json(['error' => $e->getMessage()], 404);
                }

                foreach ($results->holdings as $holding) {
                    $user_id = 1;
                    
                    $holdingObj = ([
                        'holding_id' => $holding->security_id,
                        'user_id' => $user_id,
                        'cost_basis' => $holding->cost_basis,
                        'price' => $holding->institution_price
                    ]);
                    Holdings::create($holdingObj);
                }
            });
        } catch (PlaidRequestException $e) {
            Log::error('Adding holdings failed: ' . json_encode($e->getResponse()));
            return [
                'result' => 'error',
                'message' => $e
            ];
        }

        return [
            'result' => 'success',
            'message' => 'Successfully added holdings from Plaid.'
        ];
    }

If everything looks right and you’ve gone through the Plaid link flow again, check your database for the “Holdings” table, and all the data should now be imported:

Plaid Holdings Imported

Wrapping up

This probably goes without saying, but this is just a *very* basic implementation of Plaid. There are a lot of things I’ve left out, such as error logging, issues with recursion and making duplicates, etc, but that’s outside of the scope of this tutorial.

If you’re not interesting in pulling the “Investments” endpoint from Plaid, and instead want to access a user’s bank account balance, or transaction data, those changes can be made very easily with a few minor tweaks to the code above.

Let me me know how it goes and good luck coding!

Leave a Reply

Your email address will not be published.