UNPKG

n8n-nodes-plaid

Version:

n8n community node for Plaid financial data integration - the definitive financial node for n8n

973 lines (972 loc) 46 kB
"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;