n8n-nodes-plaid
Version:
n8n community node for Plaid financial data integration - the definitive financial node for n8n
973 lines (972 loc) • 46 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Plaid = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const plaid_1 = require("plaid");
const PlaidHelpers_1 = require("../../utils/PlaidHelpers");
class Plaid {
constructor() {
this.description = {
displayName: 'Plaid',
name: 'plaid',
icon: 'file:plaid.svg',
group: ['finance'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Access Plaid financial data - transactions, accounts, and more',
defaults: {
name: 'Plaid',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'plaidApi',
required: true,
},
],
properties: [
// Resource selection
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Link',
value: 'link',
description: 'Create Link tokens and handle authentication flow',
},
{
name: 'Transaction',
value: 'transaction',
description: 'Work with bank transactions',
},
{
name: 'Account',
value: 'account',
description: 'Work with bank accounts',
},
{
name: 'Auth',
value: 'auth',
description: 'Get bank account routing and account numbers',
},
{
name: 'Institution',
value: 'institution',
description: 'Search and get information about financial institutions',
},
{
name: 'Item',
value: 'item',
description: 'Manage Plaid Items (bank connections)',
},
{
name: 'Identity',
value: 'identity',
description: 'Get account owner identity information',
},
],
default: 'transaction',
},
// Link operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['link'],
},
},
options: [
{
name: 'Create Link Token',
value: 'createToken',
description: 'Create a Link token for frontend initialization',
action: 'Create Link token',
},
{
name: 'Exchange Public Token',
value: 'exchangeToken',
description: 'Exchange a public token for an access token',
action: 'Exchange public token',
},
],
default: 'createToken',
},
// Transaction operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['transaction'],
},
},
options: [
{
name: 'Sync',
value: 'sync',
description: 'Get new/updated transactions using cursor (recommended)',
action: 'Sync transactions',
},
{
name: 'Get Range',
value: 'getRange',
description: 'Get transactions within a date range',
action: 'Get transactions in date range',
},
],
default: 'sync',
},
// Account operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['account'],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all accounts (cached data)',
action: 'Get all accounts',
},
{
name: 'Get Balances',
value: 'getBalances',
description: 'Get real-time account balances',
action: 'Get account balances',
},
],
default: 'getAll',
},
// Auth operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['auth'],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get bank account and routing numbers',
action: 'Get auth data',
},
],
default: 'get',
},
// Institution operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['institution'],
},
},
options: [
{
name: 'Search',
value: 'search',
description: 'Search for financial institutions',
action: 'Search institutions',
},
{
name: 'Get by ID',
value: 'getById',
description: 'Get institution details by ID',
action: 'Get institution by ID',
},
],
default: 'search',
},
// Item operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['item'],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get Item information',
action: 'Get item information',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove Item (disconnect bank)',
action: 'Remove item',
},
],
default: 'get',
},
// Identity operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['identity'],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get account owner identity data',
action: 'Get identity data',
},
],
default: 'get',
},
// Link operation parameters
{
displayName: 'User ID',
name: 'userId',
type: 'string',
displayOptions: {
show: {
resource: ['link'],
operation: ['createToken'],
},
},
default: 'default_user',
description: 'Unique identifier for the user in your system',
},
{
displayName: 'Products',
name: 'products',
type: 'multiOptions',
displayOptions: {
show: {
resource: ['link'],
operation: ['createToken'],
},
},
options: [
{ name: 'Transactions', value: 'transactions' },
{ name: 'Auth', value: 'auth' },
{ name: 'Identity', value: 'identity' },
{ name: 'Assets', value: 'assets' },
{ name: 'Investments', value: 'investments' },
{ name: 'Liabilities', value: 'liabilities' },
],
default: ['transactions', 'auth'],
description: 'Plaid products to enable for this Link token',
},
{
displayName: 'Public Token',
name: 'publicTokenInput',
type: 'string',
displayOptions: {
show: {
resource: ['link'],
operation: ['exchangeToken'],
},
},
required: true,
default: '',
description: 'Public token received from Plaid Link frontend',
},
// Institution operation parameters
{
displayName: 'Search Query',
name: 'searchQuery',
type: 'string',
displayOptions: {
show: {
resource: ['institution'],
operation: ['search'],
},
},
required: true,
default: '',
description: 'Search term for finding institutions (e.g., "Chase", "Bank of America")',
},
{
displayName: 'Institution ID',
name: 'institutionId',
type: 'string',
displayOptions: {
show: {
resource: ['institution'],
operation: ['getById'],
},
},
required: true,
default: '',
description: 'Plaid institution ID (e.g., ins_109508)',
},
{
displayName: 'Country Code',
name: 'countryCode',
type: 'options',
displayOptions: {
show: {
resource: ['institution'],
},
},
options: [
{ name: 'United States', value: 'US' },
{ name: 'Canada', value: 'CA' },
{ name: 'United Kingdom', value: 'GB' },
{ name: 'France', value: 'FR' },
{ name: 'Germany', value: 'DE' },
{ name: 'Spain', value: 'ES' },
{ name: 'Italy', value: 'IT' },
{ name: 'Netherlands', value: 'NL' },
],
default: 'US',
description: 'Country code for institution search',
},
// Cursor for transaction sync
{
displayName: 'Cursor',
name: 'cursor',
type: 'string',
displayOptions: {
show: {
resource: ['transaction'],
operation: ['sync'],
},
},
default: '',
description: 'Cursor for pagination (leave empty for initial sync)',
},
// Date range for transactions
{
displayName: 'Start Date',
name: 'startDate',
type: 'dateTime',
displayOptions: {
show: {
resource: ['transaction'],
operation: ['getRange'],
},
},
default: '',
description: 'Start date for transaction search (YYYY-MM-DD)',
required: true,
},
{
displayName: 'End Date',
name: 'endDate',
type: 'dateTime',
displayOptions: {
show: {
resource: ['transaction'],
operation: ['getRange'],
},
},
default: '',
description: 'End date for transaction search (YYYY-MM-DD)',
required: true,
},
// Institution search query
{
displayName: 'Search Query',
name: 'searchQuery',
type: 'string',
displayOptions: {
show: {
resource: ['institution'],
operation: ['search'],
},
},
default: '',
description: 'Search term for institutions (e.g., "Chase", "Bank of America")',
required: true,
},
// Institution ID
{
displayName: 'Institution ID',
name: 'institutionId',
type: 'string',
displayOptions: {
show: {
resource: ['institution'],
operation: ['getById'],
},
},
default: '',
description: 'Plaid institution ID (e.g., "ins_3")',
required: true,
},
// Country codes for institution search
{
displayName: 'Country',
name: 'countryCode',
type: 'options',
displayOptions: {
show: {
resource: ['institution'],
operation: ['search'],
},
},
options: [
{ name: 'United States', value: 'US' },
{ name: 'Canada', value: 'CA' },
{ name: 'United Kingdom', value: 'GB' },
{ name: 'Ireland', value: 'IE' },
{ name: 'France', value: 'FR' },
{ name: 'Spain', value: 'ES' },
{ name: 'Netherlands', value: 'NL' },
],
default: 'US',
description: 'Country for institution search',
},
// Return all vs limit
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['transaction'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['transaction'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'Max number of results to return',
},
// Account filtering
{
displayName: 'Account IDs',
name: 'accountIds',
type: 'string',
displayOptions: {
show: {
resource: ['transaction'],
},
},
default: '',
description: 'Comma-separated list of account IDs to filter (optional)',
},
// Additional options
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['transaction'],
},
},
options: [
{
displayName: 'Include Original Description',
name: 'includeOriginalDescription',
type: 'boolean',
default: false,
description: 'Include the original, unmodified description from the financial institution',
},
{
displayName: 'Include Personal Finance Category',
name: 'includePersonalFinanceCategory',
type: 'boolean',
default: true,
description: 'Include Plaid\'s enhanced personal finance categorization',
},
],
},
],
};
}
async execute() {
const items = this.getInputData();
const returnData = [];
const credentials = await this.getCredentials('plaidApi');
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
// Initialize Plaid client using modern v34.0.0 API
const environment = credentials.environment === 'production'
? plaid_1.PlaidEnvironments.production
: plaid_1.PlaidEnvironments.sandbox;
const configuration = new plaid_1.Configuration({
basePath: environment,
baseOptions: {
headers: {
'PLAID-CLIENT-ID': credentials.clientId,
'PLAID-SECRET': credentials.secret,
'Plaid-Version': '2020-09-14',
},
},
});
const client = new plaid_1.PlaidApi(configuration);
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'link') {
if (operation === 'createToken') {
// Create Link token for frontend initialization
const userId = this.getNodeParameter('userId', i);
const products = this.getNodeParameter('products', i);
const linkToken = await (0, PlaidHelpers_1.createLinkToken)(client, credentials, userId, products);
returnData.push({
json: {
link_token: linkToken,
user_id: userId,
products: products,
environment: credentials.environment,
expires_at: new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString(), // 4 hours from now
created_at: new Date().toISOString(),
source: 'plaid_link_token',
},
pairedItem: { item: i },
});
}
else if (operation === 'exchangeToken') {
// Exchange public token for access token
const publicToken = this.getNodeParameter('publicTokenInput', i);
const { accessToken, itemId } = await (0, PlaidHelpers_1.exchangePublicToken)(client, publicToken);
returnData.push({
json: {
access_token: accessToken,
item_id: itemId,
public_token: publicToken,
environment: credentials.environment,
exchanged_at: new Date().toISOString(),
source: 'plaid_token_exchange',
},
pairedItem: { item: i },
});
}
}
else if (resource === 'transaction') {
if (operation === 'sync') {
// Modern transaction sync using cursor
const cursor = this.getNodeParameter('cursor', i, '');
const returnAll = this.getNodeParameter('returnAll', i, false);
const limit = returnAll ? 500 : this.getNodeParameter('limit', i, 100);
const accountIds = this.getNodeParameter('accountIds', i, '');
const additionalFields = this.getNodeParameter('additionalFields', i, {});
// Get access token using modern authentication flow
const accessToken = await (0, PlaidHelpers_1.getAccessToken)(client, credentials);
const request = {
access_token: accessToken,
cursor: cursor || undefined,
count: limit,
};
if (accountIds) {
request.account_ids = accountIds.split(',').map(id => id.trim());
}
if (additionalFields.includeOriginalDescription) {
request.options = {
...request.options,
include_original_description: true
};
}
const response = await client.transactionsSync(request);
// Process added transactions
for (const transaction of response.data.added) {
returnData.push({
json: {
transaction_id: transaction.transaction_id,
account_id: transaction.account_id,
amount: Math.abs(transaction.amount),
transaction_type: transaction.amount < 0 ? 'expense' : 'income',
iso_currency_code: transaction.iso_currency_code,
date: transaction.date,
datetime: transaction.datetime,
name: transaction.name,
merchant_name: transaction.merchant_name,
category: transaction.category,
category_id: transaction.category_id,
personal_finance_category: transaction.personal_finance_category,
location: transaction.location,
payment_meta: transaction.payment_meta,
account_owner: transaction.account_owner,
original_description: transaction.original_description,
// Metadata
sync_status: 'added',
sync_cursor: response.data.next_cursor,
has_more: response.data.has_more,
source: 'plaid_sync',
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
// Also process modified and removed transactions
for (const transaction of response.data.modified) {
returnData.push({
json: {
...transaction,
sync_status: 'modified',
sync_cursor: response.data.next_cursor,
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
for (const removedTransaction of response.data.removed) {
returnData.push({
json: {
transaction_id: removedTransaction.transaction_id,
sync_status: 'removed',
sync_cursor: response.data.next_cursor,
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
}
else if (operation === 'getRange') {
// Legacy transaction get with date range
const startDate = this.getNodeParameter('startDate', i);
const endDate = this.getNodeParameter('endDate', i);
const returnAll = this.getNodeParameter('returnAll', i, false);
const limit = returnAll ? 500 : this.getNodeParameter('limit', i, 100);
const accountIds = this.getNodeParameter('accountIds', i, '');
const additionalFields = this.getNodeParameter('additionalFields', i, {});
// Get access token using modern authentication flow
const accessToken = await (0, PlaidHelpers_1.getAccessToken)(client, credentials);
const request = {
access_token: accessToken,
start_date: startDate.split('T')[0],
end_date: endDate.split('T')[0],
count: limit,
offset: 0,
};
if (accountIds) {
request.account_ids = accountIds.split(',').map(id => id.trim());
}
if (additionalFields.includeOriginalDescription) {
request.options = {
...request.options,
include_original_description: true
};
}
const response = await client.transactionsGet(request);
// Process transactions
for (const transaction of response.data.transactions) {
returnData.push({
json: {
transaction_id: transaction.transaction_id,
account_id: transaction.account_id,
amount: Math.abs(transaction.amount),
transaction_type: transaction.amount < 0 ? 'expense' : 'income',
iso_currency_code: transaction.iso_currency_code,
date: transaction.date,
datetime: transaction.datetime,
name: transaction.name,
merchant_name: transaction.merchant_name,
category: transaction.category,
category_id: transaction.category_id,
personal_finance_category: transaction.personal_finance_category,
location: transaction.location,
payment_meta: transaction.payment_meta,
account_owner: transaction.account_owner,
original_description: transaction.original_description,
// Metadata
total_transactions: response.data.total_transactions,
source: 'plaid_get',
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
}
}
else if (resource === 'account') {
if (operation === 'getAll') {
// Get all accounts (cached)
const accessToken = await (0, PlaidHelpers_1.getAccessToken)(client, credentials);
const request = {
access_token: accessToken,
};
const response = await client.accountsGet(request);
for (const account of response.data.accounts) {
returnData.push({
json: {
account_id: account.account_id,
persistent_account_id: account.persistent_account_id,
name: account.name,
official_name: account.official_name,
type: account.type,
subtype: account.subtype,
mask: account.mask,
balances: account.balances,
verification_status: account.verification_status,
class_type: account.class_type,
// Metadata
source: 'plaid_accounts',
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
}
else if (operation === 'getBalances') {
// Get real-time balances
const accessToken = await (0, PlaidHelpers_1.getAccessToken)(client, credentials);
const request = {
access_token: accessToken,
};
const response = await client.accountsBalanceGet(request);
for (const account of response.data.accounts) {
returnData.push({
json: {
account_id: account.account_id,
name: account.name,
type: account.type,
subtype: account.subtype,
balances: account.balances,
// Metadata
source: 'plaid_balances',
realtime: true,
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
}
}
else if (resource === 'auth') {
if (operation === 'get') {
// Get auth data (routing/account numbers)
const accessToken = await (0, PlaidHelpers_1.getAccessToken)(client, credentials);
const request = {
access_token: accessToken,
};
const response = await client.authGet(request);
for (const account of response.data.accounts) {
const authNumbers = response.data.numbers.ach?.find((ach) => ach.account_id === account.account_id);
returnData.push({
json: {
account_id: account.account_id,
name: account.name,
type: account.type,
subtype: account.subtype,
balances: account.balances,
routing_number: authNumbers?.routing,
account_number: authNumbers?.account,
wire_routing_number: authNumbers?.wire_routing,
// Metadata
source: 'plaid_auth',
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
}
}
else if (resource === 'institution') {
if (operation === 'search') {
// Search institutions
const searchQuery = this.getNodeParameter('searchQuery', i);
const countryCode = this.getNodeParameter('countryCode', i, 'US');
const request = {
query: searchQuery,
products: [plaid_1.Products.Transactions], // Default to transactions
country_codes: [countryCode],
};
const response = await client.institutionsSearch(request);
for (const institution of response.data.institutions) {
returnData.push({
json: {
institution_id: institution.institution_id,
name: institution.name,
products: institution.products,
country_codes: institution.country_codes,
url: institution.url,
primary_color: institution.primary_color,
logo: institution.logo,
routing_numbers: institution.routing_numbers,
// Metadata
source: 'plaid_institutions',
search_query: searchQuery,
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
}
else if (operation === 'getById') {
// Get institution by ID
const institutionId = this.getNodeParameter('institutionId', i);
const countryCode = this.getNodeParameter('countryCode', i, 'US');
const request = {
institution_id: institutionId,
country_codes: [countryCode],
options: {
include_optional_metadata: true,
include_status: true,
},
};
const response = await client.institutionsGetById(request);
const institution = response.data.institution;
returnData.push({
json: {
institution_id: institution.institution_id,
name: institution.name,
products: institution.products,
country_codes: institution.country_codes,
url: institution.url,
primary_color: institution.primary_color,
logo: institution.logo,
routing_numbers: institution.routing_numbers,
status: institution.status,
// Metadata
source: 'plaid_institution_details',
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
}
else if (resource === 'item') {
if (operation === 'get') {
// Get item information
const accessToken = await (0, PlaidHelpers_1.getAccessToken)(client, credentials);
const request = {
access_token: accessToken,
};
const response = await client.itemGet(request);
const item = response.data.item;
returnData.push({
json: {
item_id: item.item_id,
institution_id: item.institution_id,
webhook: item.webhook,
error: item.error,
available_products: item.available_products,
billed_products: item.billed_products,
products: item.products,
consented_products: item.consented_products,
consent_expiration_time: item.consent_expiration_time,
update_type: item.update_type,
// Metadata
source: 'plaid_item',
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
else if (operation === 'remove') {
// Remove item (disconnect bank)
const accessToken = await (0, PlaidHelpers_1.getAccessToken)(client, credentials);
const request = {
access_token: accessToken,
};
const response = await client.itemRemove(request);
returnData.push({
json: {
removed: true,
request_id: response.data.request_id,
// Metadata
source: 'plaid_item_remove',
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
}
else if (resource === 'identity') {
if (operation === 'get') {
// Get identity data
const accessToken = await (0, PlaidHelpers_1.getAccessToken)(client, credentials);
const request = {
access_token: accessToken,
};
const response = await client.identityGet(request);
for (const account of response.data.accounts) {
returnData.push({
json: {
account_id: account.account_id,
name: account.name,
type: account.type,
subtype: account.subtype,
balances: account.balances,
owners: account.owners,
// Metadata
source: 'plaid_identity',
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
}
}
}
}
catch (error) {
// Enhanced error handling for Plaid API errors
let errorMessage = 'Unknown error occurred';
let errorCode = 'UNKNOWN';
if (error.response?.data) {
// Plaid API error
const plaidError = error.response.data;
errorMessage = plaidError.error_message || plaidError.display_message || error.message;
errorCode = plaidError.error_code || 'PLAID_ERROR';
}
else {
// Network or other error
errorMessage = error.message;
}
if (this.continueOnFail()) {
returnData.push({
json: {
error: true,
error_message: errorMessage,
error_code: errorCode,
resource,
operation,
processed_at: new Date().toISOString(),
},
pairedItem: { item: i },
});
continue;
}
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Plaid API Error (${errorCode}): ${errorMessage}`, { itemIndex: i });
}
}
return [returnData];
}
}
exports.Plaid = Plaid;