mrnewport / laravel-plaid
A comprehensive Laravel package for Plaid API integration
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.0
- illuminate/config: ^10.0|^11.0|^12.0
- illuminate/container: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.0
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0|^10.0
- pestphp/pest: ^2.0
- pestphp/pest-plugin-laravel: ^2.0
- phpunit/phpunit: ^10.0
README
A comprehensive, production-ready Laravel package for the Plaid API, providing complete coverage of all Plaid products and endpoints with a clean, intuitive interface.
Features
- ๐ Complete API Coverage: Every Plaid endpoint is implemented
- ๐ Type-Safe: Full PHP type hints and DTOs for all responses
- ๐ก๏ธ Production Ready: Automatic retries, error handling, and logging
- ๐งช Fully Tested: 100% test coverage with Pest
- ๐ Well Documented: Clear examples and comprehensive documentation
- โก Laravel Native: Service provider, facades, and config publishing
- ๐ Auto Retry: Configurable retry logic for failed requests
- ๐ Request Logging: Optional request/response logging with sensitive data redaction
Requirements
- PHP 8.1 or higher
- Laravel 10.x, 11.x, or 12.x
Installation
Install the package via Composer:
composer require mrnewport/laravel-plaid
Configuration
Publish the configuration file:
php artisan vendor:publish --provider="MrNewport\LaravelPlaid\PlaidServiceProvider" --tag="plaid-config"
Add your Plaid credentials to your .env
file:
PLAID_CLIENT_ID=your_client_id PLAID_SECRET=your_secret_key PLAID_ENVIRONMENT=sandbox # sandbox, development, or production PLAID_VERSION=2020-09-14
Optional Configuration
# HTTP Client Options PLAID_TIMEOUT=30 PLAID_CONNECT_TIMEOUT=10 PLAID_RETRY_ENABLED=true PLAID_RETRY_MAX_ATTEMPTS=3 PLAID_RETRY_DELAY=1000 # Webhook Configuration PLAID_WEBHOOK_SECRET=your_webhook_secret PLAID_WEBHOOK_TOLERANCE=300 # Logging PLAID_LOGGING_ENABLED=false PLAID_LOG_CHANNEL=stack
Usage
Basic Usage
use MrNewport\LaravelPlaid\Facades\Plaid; // Create a link token for Plaid Link $response = Plaid::linkToken()->create([ 'user' => [ 'client_user_id' => 'user-' . $user->id, ], 'products' => ['auth', 'transactions'], 'client_name' => 'Your App Name', 'country_codes' => ['US'], 'language' => 'en', 'webhook' => 'https://yourapp.com/webhooks/plaid', ]); $linkToken = $response['link_token'];
Exchange Public Token
// After user completes Plaid Link $response = Plaid::item()->publicTokenExchange($publicToken); $accessToken = $response['access_token']; $itemId = $response['item_id']; // Store these securely in your database $user->plaid_access_token = encrypt($accessToken); $user->plaid_item_id = $itemId; $user->save();
Get Accounts
$response = Plaid::accounts()->get($accessToken); foreach ($response['accounts'] as $account) { echo $account->name . ': $' . $account->balances['current'] . PHP_EOL; }
Get Transactions
// Using the new Transactions Sync endpoint (recommended) $cursor = $user->plaid_cursor; // Store cursor for incremental updates $hasMore = true; while ($hasMore) { $response = Plaid::transactions()->sync($accessToken, $cursor); foreach ($response['added'] as $transaction) { // Process new transactions Transaction::create([ 'account_id' => $transaction->account_id, 'amount' => $transaction->amount, 'name' => $transaction->name, 'date' => $transaction->date, 'category' => $transaction->category, ]); } foreach ($response['modified'] as $transaction) { // Update existing transactions } foreach ($response['removed'] as $removed) { // Delete removed transactions } $cursor = $response['next_cursor']; $hasMore = $response['has_more']; } // Save cursor for next sync $user->plaid_cursor = $cursor; $user->save();
Get Auth Information
$response = Plaid::auth()->get($accessToken); foreach ($response['accounts'] as $account) { $accountNumbers = $response['numbers']->ach; foreach ($accountNumbers as $ach) { if ($ach['account_id'] === $account['account_id']) { echo "Routing: {$ach['routing']}, Account: {$ach['account']}" . PHP_EOL; } } }
Create ACH Transfer
// First, create a transfer authorization $authResponse = Plaid::transfer()->authorizationCreate([ 'access_token' => $accessToken, 'account_id' => $accountId, 'type' => 'debit', 'network' => 'ach', 'amount' => '100.00', 'ach_class' => 'ppd', 'user' => [ 'legal_name' => 'John Doe', 'email_address' => 'john@example.com', ], ]); // Then create the transfer if ($authResponse['authorization']['decision'] === 'approved') { $transferResponse = Plaid::transfer()->create([ 'access_token' => $accessToken, 'account_id' => $accountId, 'authorization_id' => $authResponse['authorization']['id'], 'description' => 'Payment for order #1234', ]); $transferId = $transferResponse['transfer']['id']; }
Identity Verification
$response = Plaid::identityVerification()->create([ 'template_id' => 'idvtmp_xxxxx', 'gave_consent' => true, 'user' => [ 'client_user_id' => 'user-' . $user->id, 'email_address' => $user->email, 'phone_number' => $user->phone, 'date_of_birth' => $user->date_of_birth, 'name' => [ 'given_name' => $user->first_name, 'family_name' => $user->last_name, ], 'address' => [ 'street' => $user->street_address, 'city' => $user->city, 'region' => $user->state, 'postal_code' => $user->zip, 'country' => 'US', ], ], ]);
Investment Data
// Get investment holdings $holdings = Plaid::investments()->holdingsGet($accessToken); foreach ($holdings['holdings'] as $holding) { echo "{$holding['quantity']} shares of {$holding['security_id']}" . PHP_EOL; } // Get investment transactions $transactions = Plaid::investments()->transactionsGet( $accessToken, '2024-01-01', '2024-12-31' );
Create Processor Token
// For Stripe $response = Plaid::processor()->stripeTokenCreate($accessToken, $accountId); $stripeToken = $response['stripe_bank_account_token']; // For Dwolla $response = Plaid::processor()->dwollaTokenCreate($accessToken, $accountId); $processorToken = $response['processor_token']; // For other processors $response = Plaid::processor()->tokenCreate($accessToken, $accountId, 'achq'); $processorToken = $response['processor_token'];
Available Services
Core Services
- Accounts:
Plaid::accounts()
- Account information and balances - Auth:
Plaid::auth()
- Account and routing numbers - Transactions:
Plaid::transactions()
- Transaction data and categorization - Identity:
Plaid::identity()
- Account holder information - Balance:
Plaid::accounts()->getBalance()
- Real-time balance
Wealth & Investments
- Investments:
Plaid::investments()
- Investment holdings and transactions - Liabilities:
Plaid::liabilities()
- Loan and credit card data
Income & Employment
- Income:
Plaid::income()
- Income verification - Employment:
Plaid::employment()
- Employment verification - Assets:
Plaid::assets()
- Asset reports for lending
Payments & Transfers
- Transfer:
Plaid::transfer()
- ACH transfers and payments - Payment Initiation:
Plaid::paymentInitiation()
- UK/EU payments
Risk & Compliance
- Identity Verification:
Plaid::identityVerification()
- KYC/AML - Monitor: Built into relevant services
- Beacon: Built into relevant services
Other Services
- Link Token:
Plaid::linkToken()
- Create and manage Link tokens - Item:
Plaid::item()
- Manage Items (connections) - Institutions:
Plaid::institutions()
- Institution information - Processor:
Plaid::processor()
- Processor tokens - Statements:
Plaid::statements()
- PDF statements - Sandbox:
Plaid::sandbox()
- Testing utilities
Error Handling
The package provides specific exception types for different error scenarios:
use MrNewport\LaravelPlaid\Exceptions\PlaidException; use MrNewport\LaravelPlaid\Exceptions\PlaidAuthenticationException; use MrNewport\LaravelPlaid\Exceptions\PlaidRateLimitException; use MrNewport\LaravelPlaid\Exceptions\PlaidRequestException; try { $accounts = Plaid::accounts()->get($accessToken); } catch (PlaidAuthenticationException $e) { // Invalid API keys Log::error('Plaid authentication failed: ' . $e->getMessage()); } catch (PlaidRateLimitException $e) { // Rate limit exceeded Log::warning('Plaid rate limit hit: ' . $e->getMessage()); } catch (PlaidRequestException $e) { // Invalid request (400 errors) Log::error('Invalid Plaid request: ' . $e->getMessage()); Log::error('Error code: ' . $e->getErrorCode()); Log::error('Error type: ' . $e->getErrorType()); } catch (PlaidException $e) { // Other Plaid errors Log::error('Plaid error: ' . $e->getMessage()); }
Webhooks
Handle Plaid webhooks in your application:
Route::post('/webhooks/plaid', function (Request $request) { $webhookType = $request->input('webhook_type'); $webhookCode = $request->input('webhook_code'); switch ($webhookType) { case 'TRANSACTIONS': if ($webhookCode === 'SYNC_UPDATES_AVAILABLE') { // Sync new transactions $itemId = $request->input('item_id'); dispatch(new SyncPlaidTransactions($itemId)); } break; case 'ITEM': if ($webhookCode === 'ERROR') { // Handle item errors $error = $request->input('error'); // Notify user to re-authenticate } break; } return response()->json(['status' => 'ok']); });
Logging
Enable request/response logging for debugging:
PLAID_LOGGING_ENABLED=true PLAID_LOG_CHANNEL=plaid
Configure the log channel in config/logging.php
:
'channels' => [ 'plaid' => [ 'driver' => 'daily', 'path' => storage_path('logs/plaid.log'), 'level' => 'debug', 'days' => 7, ], ],
Sensitive fields are automatically redacted from logs.
Testing
The package includes comprehensive test coverage using Pest:
# Run all tests composer test # Run with coverage composer test-coverage # Run specific test file vendor/bin/pest tests/Unit/Services/TransactionsServiceTest.php
Mocking in Tests
use MrNewport\LaravelPlaid\Facades\Plaid; // In your test Plaid::shouldReceive('accounts->get') ->once() ->with($accessToken) ->andReturn([ 'accounts' => [ ['account_id' => 'test123', 'name' => 'Checking'], ], ]);
Sandbox Testing
Use the sandbox environment for testing:
// Create sandbox public token $publicToken = Plaid::sandbox()->publicTokenCreate( 'ins_109508', ['auth', 'transactions'] ); // Fire a webhook in sandbox Plaid::sandbox()->itemFireWebhook($accessToken, 'DEFAULT_UPDATE'); // Simulate transfer events Plaid::sandbox()->transferSimulate($transferId, 'posted');
Advanced Usage
Custom HTTP Client Options
// config/plaid.php 'http_options' => [ 'timeout' => 60, 'connect_timeout' => 10, 'retry_enabled' => true, 'retry_max_attempts' => 5, 'retry_delay' => 2000, 'proxy' => 'tcp://localhost:8080', ],
Using Without Facade
use MrNewport\LaravelPlaid\Plaid; use MrNewport\LaravelPlaid\PlaidClient; // Inject via constructor public function __construct(private Plaid $plaid) { } // Or resolve from container $plaid = app(Plaid::class); $accounts = $plaid->accounts()->get($accessToken);
Direct Client Access
$client = Plaid::getClient(); $response = $client->post('/custom/endpoint', ['data' => 'value']);
Troubleshooting
Common Issues
-
SSL Certificate Issues
# Download latest CA bundle curl -o /path/to/cacert.pem https://curl.se/ca/cacert.pem
Configure in your
.env
:CURL_CA_BUNDLE=/path/to/cacert.pem
-
Rate Limiting The package automatically retries on rate limit errors. Adjust retry settings:
PLAID_RETRY_MAX_ATTEMPTS=5 PLAID_RETRY_DELAY=5000
-
Timeout Issues Increase timeout for large requests:
PLAID_TIMEOUT=120 PLAID_CONNECT_TIMEOUT=30
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
Security
If you discover any security-related issues, please email admin@matthewnewport.com instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.
Support
For support, please email admin@matthewnewport.com or create an issue on GitHub.
Changelog
Please see CHANGELOG.md for recent changes.
Built with โค๏ธ for the Laravel community