chub-dev 0.1.0 → 0.1.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/bin/chub-mcp +2 -0
- package/dist/airtable/docs/database/javascript/DOC.md +1437 -0
- package/dist/airtable/docs/database/python/DOC.md +1735 -0
- package/dist/amplitude/docs/analytics/javascript/DOC.md +1282 -0
- package/dist/amplitude/docs/analytics/python/DOC.md +1199 -0
- package/dist/anthropic/docs/claude-api/javascript/DOC.md +503 -0
- package/dist/anthropic/docs/claude-api/python/DOC.md +389 -0
- package/dist/asana/docs/tasks/DOC.md +1396 -0
- package/dist/assemblyai/docs/transcription/DOC.md +1043 -0
- package/dist/atlassian/docs/confluence/javascript/DOC.md +1347 -0
- package/dist/atlassian/docs/confluence/python/DOC.md +1604 -0
- package/dist/auth0/docs/identity/javascript/DOC.md +968 -0
- package/dist/auth0/docs/identity/python/DOC.md +1199 -0
- package/dist/aws/docs/s3/javascript/DOC.md +1773 -0
- package/dist/aws/docs/s3/python/DOC.md +1807 -0
- package/dist/binance/docs/trading/javascript/DOC.md +1315 -0
- package/dist/binance/docs/trading/python/DOC.md +1454 -0
- package/dist/braintree/docs/gateway/javascript/DOC.md +1278 -0
- package/dist/braintree/docs/gateway/python/DOC.md +1179 -0
- package/dist/chromadb/docs/embeddings-db/javascript/DOC.md +1263 -0
- package/dist/chromadb/docs/embeddings-db/python/DOC.md +1707 -0
- package/dist/clerk/docs/auth/javascript/DOC.md +1220 -0
- package/dist/clerk/docs/auth/python/DOC.md +274 -0
- package/dist/cloudflare/docs/workers/javascript/DOC.md +918 -0
- package/dist/cloudflare/docs/workers/python/DOC.md +994 -0
- package/dist/cockroachdb/docs/distributed-db/DOC.md +1500 -0
- package/dist/cohere/docs/llm/DOC.md +1335 -0
- package/dist/datadog/docs/monitoring/javascript/DOC.md +1740 -0
- package/dist/datadog/docs/monitoring/python/DOC.md +1815 -0
- package/dist/deepgram/docs/speech/javascript/DOC.md +885 -0
- package/dist/deepgram/docs/speech/python/DOC.md +685 -0
- package/dist/deepl/docs/translation/javascript/DOC.md +887 -0
- package/dist/deepl/docs/translation/python/DOC.md +944 -0
- package/dist/deepseek/docs/llm/DOC.md +1220 -0
- package/dist/directus/docs/headless-cms/javascript/DOC.md +1128 -0
- package/dist/directus/docs/headless-cms/python/DOC.md +1276 -0
- package/dist/discord/docs/bot/javascript/DOC.md +1090 -0
- package/dist/discord/docs/bot/python/DOC.md +1130 -0
- package/dist/elasticsearch/docs/search/DOC.md +1634 -0
- package/dist/elevenlabs/docs/text-to-speech/javascript/DOC.md +336 -0
- package/dist/elevenlabs/docs/text-to-speech/python/DOC.md +552 -0
- package/dist/firebase/docs/auth/DOC.md +1015 -0
- package/dist/gemini/docs/genai/javascript/DOC.md +691 -0
- package/dist/gemini/docs/genai/python/DOC.md +555 -0
- package/dist/github/docs/octokit/DOC.md +1560 -0
- package/dist/google/docs/bigquery/javascript/DOC.md +1688 -0
- package/dist/google/docs/bigquery/python/DOC.md +1503 -0
- package/dist/hubspot/docs/crm/javascript/DOC.md +1805 -0
- package/dist/hubspot/docs/crm/python/DOC.md +2033 -0
- package/dist/huggingface/docs/transformers/DOC.md +948 -0
- package/dist/intercom/docs/messaging/javascript/DOC.md +1844 -0
- package/dist/intercom/docs/messaging/python/DOC.md +1797 -0
- package/dist/jira/docs/issues/javascript/DOC.md +1420 -0
- package/dist/jira/docs/issues/python/DOC.md +1492 -0
- package/dist/kafka/docs/streaming/javascript/DOC.md +1671 -0
- package/dist/kafka/docs/streaming/python/DOC.md +1464 -0
- package/dist/landingai-ade/docs/api/DOC.md +620 -0
- package/dist/landingai-ade/docs/sdk/python/DOC.md +489 -0
- package/dist/landingai-ade/docs/sdk/typescript/DOC.md +542 -0
- package/dist/landingai-ade/skills/SKILL.md +489 -0
- package/dist/launchdarkly/docs/feature-flags/javascript/DOC.md +1191 -0
- package/dist/launchdarkly/docs/feature-flags/python/DOC.md +1671 -0
- package/dist/linear/docs/tracker/DOC.md +1554 -0
- package/dist/livekit/docs/realtime/javascript/DOC.md +303 -0
- package/dist/livekit/docs/realtime/python/DOC.md +163 -0
- package/dist/mailchimp/docs/marketing/DOC.md +1420 -0
- package/dist/meilisearch/docs/search/DOC.md +1241 -0
- package/dist/microsoft/docs/onedrive/javascript/DOC.md +1421 -0
- package/dist/microsoft/docs/onedrive/python/DOC.md +1549 -0
- package/dist/mongodb/docs/atlas/DOC.md +2041 -0
- package/dist/notion/docs/workspace-api/javascript/DOC.md +1435 -0
- package/dist/notion/docs/workspace-api/python/DOC.md +1400 -0
- package/dist/okta/docs/identity/javascript/DOC.md +1171 -0
- package/dist/okta/docs/identity/python/DOC.md +1401 -0
- package/dist/openai/docs/chat/javascript/DOC.md +407 -0
- package/dist/openai/docs/chat/python/DOC.md +568 -0
- package/dist/paypal/docs/checkout/DOC.md +278 -0
- package/dist/pinecone/docs/sdk/javascript/DOC.md +984 -0
- package/dist/pinecone/docs/sdk/python/DOC.md +1395 -0
- package/dist/plaid/docs/banking/javascript/DOC.md +1163 -0
- package/dist/plaid/docs/banking/python/DOC.md +1203 -0
- package/dist/playwright-community/skills/login-flows/SKILL.md +108 -0
- package/dist/postmark/docs/transactional-email/DOC.md +1168 -0
- package/dist/prisma/docs/orm/javascript/DOC.md +1419 -0
- package/dist/prisma/docs/orm/python/DOC.md +1317 -0
- package/dist/qdrant/docs/vector-search/javascript/DOC.md +1221 -0
- package/dist/qdrant/docs/vector-search/python/DOC.md +1653 -0
- package/dist/rabbitmq/docs/message-queue/javascript/DOC.md +1193 -0
- package/dist/rabbitmq/docs/message-queue/python/DOC.md +1243 -0
- package/dist/razorpay/docs/payments/javascript/DOC.md +1219 -0
- package/dist/razorpay/docs/payments/python/DOC.md +1330 -0
- package/dist/redis/docs/key-value/javascript/DOC.md +1851 -0
- package/dist/redis/docs/key-value/python/DOC.md +2054 -0
- package/dist/registry.json +2817 -0
- package/dist/replicate/docs/model-hosting/DOC.md +1318 -0
- package/dist/resend/docs/email/DOC.md +1271 -0
- package/dist/salesforce/docs/crm/javascript/DOC.md +1241 -0
- package/dist/salesforce/docs/crm/python/DOC.md +1183 -0
- package/dist/search-index.json +1 -0
- package/dist/sendgrid/docs/email-api/javascript/DOC.md +371 -0
- package/dist/sendgrid/docs/email-api/python/DOC.md +656 -0
- package/dist/sentry/docs/error-tracking/javascript/DOC.md +1073 -0
- package/dist/sentry/docs/error-tracking/python/DOC.md +1309 -0
- package/dist/shopify/docs/storefront/DOC.md +457 -0
- package/dist/slack/docs/workspace/javascript/DOC.md +933 -0
- package/dist/slack/docs/workspace/python/DOC.md +271 -0
- package/dist/square/docs/payments/javascript/DOC.md +1855 -0
- package/dist/square/docs/payments/python/DOC.md +1728 -0
- package/dist/stripe/docs/api/DOC.md +1727 -0
- package/dist/stripe/docs/payments/DOC.md +1726 -0
- package/dist/stytch/docs/auth/javascript/DOC.md +1813 -0
- package/dist/stytch/docs/auth/python/DOC.md +1962 -0
- package/dist/supabase/docs/client/DOC.md +1606 -0
- package/dist/twilio/docs/messaging/python/DOC.md +469 -0
- package/dist/twilio/docs/messaging/typescript/DOC.md +946 -0
- package/dist/vercel/docs/platform/DOC.md +1940 -0
- package/dist/weaviate/docs/vector-db/javascript/DOC.md +1268 -0
- package/dist/weaviate/docs/vector-db/python/DOC.md +1388 -0
- package/dist/zendesk/docs/support/javascript/DOC.md +2150 -0
- package/dist/zendesk/docs/support/python/DOC.md +2297 -0
- package/package.json +22 -6
- package/skills/get-api-docs/SKILL.md +84 -0
- package/src/commands/annotate.js +83 -0
- package/src/commands/build.js +12 -1
- package/src/commands/feedback.js +150 -0
- package/src/commands/get.js +83 -42
- package/src/commands/search.js +7 -0
- package/src/index.js +43 -17
- package/src/lib/analytics.js +90 -0
- package/src/lib/annotations.js +57 -0
- package/src/lib/bm25.js +170 -0
- package/src/lib/cache.js +69 -6
- package/src/lib/config.js +8 -3
- package/src/lib/identity.js +99 -0
- package/src/lib/registry.js +103 -20
- package/src/lib/telemetry.js +86 -0
- package/src/mcp/server.js +177 -0
- package/src/mcp/tools.js +251 -0
|
@@ -0,0 +1,1163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: banking
|
|
3
|
+
description: "Plaid API Coding Guidelines for JavaScript/TypeScript using the official Plaid libraries and SDKs"
|
|
4
|
+
metadata:
|
|
5
|
+
languages: "javascript"
|
|
6
|
+
versions: "39.1.0"
|
|
7
|
+
updated-on: "2026-03-02"
|
|
8
|
+
source: maintainer
|
|
9
|
+
tags: "plaid,banking,fintech,payments,financial-data"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Plaid API Coding Guidelines (JavaScript/TypeScript)
|
|
13
|
+
|
|
14
|
+
You are a Plaid API coding expert. Help me with writing code using the Plaid API calling the official libraries and SDKs.
|
|
15
|
+
|
|
16
|
+
## Golden Rule: Use the Correct and Current SDK
|
|
17
|
+
|
|
18
|
+
Always use the official Plaid Node.js SDK for all Plaid API interactions.
|
|
19
|
+
|
|
20
|
+
- **Library Name:** Plaid Node.js SDK
|
|
21
|
+
- **NPM Package:** `plaid`
|
|
22
|
+
- **Current Version:** 39.1.0
|
|
23
|
+
|
|
24
|
+
**Installation:**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install plaid
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Import Patterns:**
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// Correct - ES6 import
|
|
34
|
+
import { Configuration, PlaidApi, PlaidEnvironments } from 'plaid';
|
|
35
|
+
|
|
36
|
+
// Correct - CommonJS
|
|
37
|
+
const { Configuration, PlaidApi, PlaidEnvironments } = require('plaid');
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Important Notes:**
|
|
41
|
+
|
|
42
|
+
- The plaid-node client library is updated monthly
|
|
43
|
+
- Always use a recent version of the library for new endpoints and fields support
|
|
44
|
+
- This library uses semantic versioning with breaking changes indicated by major version bumps
|
|
45
|
+
- The library is generated from the OpenAPI spec
|
|
46
|
+
|
|
47
|
+
## Initialization and Authentication
|
|
48
|
+
|
|
49
|
+
The Plaid library requires creating a `Configuration` object and `PlaidApi` client instance for all API calls.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { Configuration, PlaidApi, PlaidEnvironments } from 'plaid';
|
|
53
|
+
|
|
54
|
+
const configuration = new Configuration({
|
|
55
|
+
basePath: PlaidEnvironments.sandbox,
|
|
56
|
+
baseOptions: {
|
|
57
|
+
headers: {
|
|
58
|
+
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
|
|
59
|
+
'PLAID-SECRET': process.env.PLAID_SECRET,
|
|
60
|
+
'Plaid-Version': '2020-09-14',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const plaidClient = new PlaidApi(configuration);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Environment Configuration
|
|
69
|
+
|
|
70
|
+
Plaid has multiple environments for different use cases:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// Sandbox - for testing with stateful test data
|
|
74
|
+
const configuration = new Configuration({
|
|
75
|
+
basePath: PlaidEnvironments.sandbox,
|
|
76
|
+
baseOptions: {
|
|
77
|
+
headers: {
|
|
78
|
+
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
|
|
79
|
+
'PLAID-SECRET': process.env.PLAID_SANDBOX_SECRET,
|
|
80
|
+
'Plaid-Version': '2020-09-14',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Development - for testing with live credentials
|
|
86
|
+
const configuration = new Configuration({
|
|
87
|
+
basePath: PlaidEnvironments.development,
|
|
88
|
+
baseOptions: {
|
|
89
|
+
headers: {
|
|
90
|
+
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
|
|
91
|
+
'PLAID-SECRET': process.env.PLAID_DEVELOPMENT_SECRET,
|
|
92
|
+
'Plaid-Version': '2020-09-14',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Production - for live users
|
|
98
|
+
const configuration = new Configuration({
|
|
99
|
+
basePath: PlaidEnvironments.production,
|
|
100
|
+
baseOptions: {
|
|
101
|
+
headers: {
|
|
102
|
+
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
|
|
103
|
+
'PLAID-SECRET': process.env.PLAID_PRODUCTION_SECRET,
|
|
104
|
+
'Plaid-Version': '2020-09-14',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Environment Variables Setup
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# .env file
|
|
114
|
+
PLAID_CLIENT_ID=your_client_id_here
|
|
115
|
+
PLAID_SANDBOX_SECRET=your_sandbox_secret_here
|
|
116
|
+
PLAID_DEVELOPMENT_SECRET=your_development_secret_here
|
|
117
|
+
PLAID_PRODUCTION_SECRET=your_production_secret_here
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Load environment variables
|
|
122
|
+
import dotenv from 'dotenv';
|
|
123
|
+
dotenv.config();
|
|
124
|
+
|
|
125
|
+
const configuration = new Configuration({
|
|
126
|
+
basePath: PlaidEnvironments.sandbox,
|
|
127
|
+
baseOptions: {
|
|
128
|
+
headers: {
|
|
129
|
+
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
|
|
130
|
+
'PLAID-SECRET': process.env.PLAID_SANDBOX_SECRET,
|
|
131
|
+
'Plaid-Version': '2020-09-14',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Core Plaid Flow
|
|
138
|
+
|
|
139
|
+
The standard Plaid integration flow follows these steps:
|
|
140
|
+
|
|
141
|
+
1. Create a link_token
|
|
142
|
+
2. Initialize Plaid Link on the frontend
|
|
143
|
+
3. Receive a public_token from Link
|
|
144
|
+
4. Exchange the public_token for an access_token
|
|
145
|
+
5. Use the access_token to make API requests
|
|
146
|
+
|
|
147
|
+
### Step 1: Create Link Token
|
|
148
|
+
|
|
149
|
+
Create a temporary link_token to authenticate your app with Plaid Link:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import {
|
|
153
|
+
Configuration,
|
|
154
|
+
PlaidApi,
|
|
155
|
+
PlaidEnvironments,
|
|
156
|
+
Products,
|
|
157
|
+
CountryCode
|
|
158
|
+
} from 'plaid';
|
|
159
|
+
|
|
160
|
+
const plaidClient = new PlaidApi(configuration);
|
|
161
|
+
|
|
162
|
+
async function createLinkToken(userId: string) {
|
|
163
|
+
const request = {
|
|
164
|
+
user: {
|
|
165
|
+
client_user_id: userId,
|
|
166
|
+
},
|
|
167
|
+
client_name: 'My Application',
|
|
168
|
+
products: [Products.Auth, Products.Transactions],
|
|
169
|
+
country_codes: [CountryCode.Us],
|
|
170
|
+
language: 'en',
|
|
171
|
+
webhook: 'https://your-domain.com/plaid/webhook',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const response = await plaidClient.linkTokenCreate(request);
|
|
176
|
+
const linkToken = response.data.link_token;
|
|
177
|
+
return linkToken;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('Error creating link token:', error);
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Step 2: Exchange Public Token for Access Token
|
|
186
|
+
|
|
187
|
+
After the user completes the Link flow, exchange the public_token for a permanent access_token:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
async function exchangePublicToken(publicToken: string) {
|
|
191
|
+
try {
|
|
192
|
+
const response = await plaidClient.itemPublicTokenExchange({
|
|
193
|
+
public_token: publicToken,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const accessToken = response.data.access_token;
|
|
197
|
+
const itemId = response.data.item_id;
|
|
198
|
+
|
|
199
|
+
// Store these securely in your database
|
|
200
|
+
console.log('Access Token:', accessToken);
|
|
201
|
+
console.log('Item ID:', itemId);
|
|
202
|
+
|
|
203
|
+
return { accessToken, itemId };
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('Error exchanging public token:', error);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Product APIs
|
|
212
|
+
|
|
213
|
+
### Auth - Account and Routing Numbers
|
|
214
|
+
|
|
215
|
+
Retrieve bank account and routing numbers for ACH transfers:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
async function getAuthData(accessToken: string) {
|
|
219
|
+
try {
|
|
220
|
+
const request = {
|
|
221
|
+
access_token: accessToken,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const response = await plaidClient.authGet(request);
|
|
225
|
+
const accounts = response.data.accounts;
|
|
226
|
+
const numbers = response.data.numbers;
|
|
227
|
+
|
|
228
|
+
console.log('Accounts:', accounts);
|
|
229
|
+
console.log('Account Numbers:', numbers.ach);
|
|
230
|
+
console.log('Routing Numbers:', numbers.ach[0].routing);
|
|
231
|
+
|
|
232
|
+
return { accounts, numbers };
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('Error getting auth data:', error);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Advanced Auth with Options:**
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
async function getAuthDataWithOptions(accessToken: string, accountIds?: string[]) {
|
|
244
|
+
try {
|
|
245
|
+
const request = {
|
|
246
|
+
access_token: accessToken,
|
|
247
|
+
options: {
|
|
248
|
+
account_ids: accountIds, // Optional: filter specific accounts
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const response = await plaidClient.authGet(request);
|
|
253
|
+
return response.data;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error('Error getting auth data:', error);
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Accounts - Retrieve Account Information
|
|
262
|
+
|
|
263
|
+
Get account details including balances and metadata:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
async function getAccounts(accessToken: string) {
|
|
267
|
+
try {
|
|
268
|
+
const request = {
|
|
269
|
+
access_token: accessToken,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const response = await plaidClient.accountsGet(request);
|
|
273
|
+
const accounts = response.data.accounts;
|
|
274
|
+
|
|
275
|
+
accounts.forEach(account => {
|
|
276
|
+
console.log('Account ID:', account.account_id);
|
|
277
|
+
console.log('Name:', account.name);
|
|
278
|
+
console.log('Type:', account.type);
|
|
279
|
+
console.log('Subtype:', account.subtype);
|
|
280
|
+
console.log('Current Balance:', account.balances.current);
|
|
281
|
+
console.log('Available Balance:', account.balances.available);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return accounts;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error('Error getting accounts:', error);
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Balance - Real-time Balance Information
|
|
293
|
+
|
|
294
|
+
Get up-to-date balance information:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
async function getBalance(accessToken: string) {
|
|
298
|
+
try {
|
|
299
|
+
const request = {
|
|
300
|
+
access_token: accessToken,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const response = await plaidClient.accountsBalanceGet(request);
|
|
304
|
+
const accounts = response.data.accounts;
|
|
305
|
+
|
|
306
|
+
accounts.forEach(account => {
|
|
307
|
+
console.log(`${account.name}: $${account.balances.current}`);
|
|
308
|
+
console.log(`Available: $${account.balances.available}`);
|
|
309
|
+
console.log(`Currency: ${account.balances.iso_currency_code}`);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return accounts;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error('Error getting balance:', error);
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Advanced Balance with Account Filtering:**
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
async function getBalanceForSpecificAccounts(
|
|
324
|
+
accessToken: string,
|
|
325
|
+
accountIds: string[]
|
|
326
|
+
) {
|
|
327
|
+
try {
|
|
328
|
+
const request = {
|
|
329
|
+
access_token: accessToken,
|
|
330
|
+
options: {
|
|
331
|
+
account_ids: accountIds,
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const response = await plaidClient.accountsBalanceGet(request);
|
|
336
|
+
return response.data.accounts;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error('Error getting balance:', error);
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Identity - Account Holder Information
|
|
345
|
+
|
|
346
|
+
Retrieve identity information for account holders:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
async function getIdentity(accessToken: string) {
|
|
350
|
+
try {
|
|
351
|
+
const request = {
|
|
352
|
+
access_token: accessToken,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const response = await plaidClient.identityGet(request);
|
|
356
|
+
const accounts = response.data.accounts;
|
|
357
|
+
|
|
358
|
+
accounts.forEach(account => {
|
|
359
|
+
console.log('Account:', account.name);
|
|
360
|
+
account.owners.forEach(owner => {
|
|
361
|
+
console.log('Name:', owner.names);
|
|
362
|
+
console.log('Email:', owner.emails);
|
|
363
|
+
console.log('Phone:', owner.phone_numbers);
|
|
364
|
+
console.log('Address:', owner.addresses);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
return response.data;
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error('Error getting identity:', error);
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Transactions - Transaction History
|
|
377
|
+
|
|
378
|
+
Plaid provides two methods for retrieving transactions: `/transactions/get` (legacy) and `/transactions/sync` (recommended).
|
|
379
|
+
|
|
380
|
+
#### Transactions Sync (Recommended)
|
|
381
|
+
|
|
382
|
+
The `/transactions/sync` endpoint provides incremental updates and is the recommended approach:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
async function syncTransactions(accessToken: string, cursor?: string) {
|
|
386
|
+
try {
|
|
387
|
+
const request = {
|
|
388
|
+
access_token: accessToken,
|
|
389
|
+
cursor: cursor || undefined,
|
|
390
|
+
count: 100, // Number of transactions to fetch (max 500)
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const response = await plaidClient.transactionsSync(request);
|
|
394
|
+
|
|
395
|
+
console.log('Added transactions:', response.data.added.length);
|
|
396
|
+
console.log('Modified transactions:', response.data.modified.length);
|
|
397
|
+
console.log('Removed transactions:', response.data.removed.length);
|
|
398
|
+
console.log('Next cursor:', response.data.next_cursor);
|
|
399
|
+
console.log('Has more:', response.data.has_more);
|
|
400
|
+
|
|
401
|
+
// Process transactions
|
|
402
|
+
response.data.added.forEach(transaction => {
|
|
403
|
+
console.log('Date:', transaction.date);
|
|
404
|
+
console.log('Name:', transaction.name);
|
|
405
|
+
console.log('Amount:', transaction.amount);
|
|
406
|
+
console.log('Category:', transaction.category);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return response.data;
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.error('Error syncing transactions:', error);
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Paginated Transaction Sync:**
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
async function getAllTransactions(accessToken: string) {
|
|
421
|
+
let cursor: string | undefined = undefined;
|
|
422
|
+
let allTransactions: any[] = [];
|
|
423
|
+
let hasMore = true;
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
while (hasMore) {
|
|
427
|
+
const request = {
|
|
428
|
+
access_token: accessToken,
|
|
429
|
+
cursor: cursor,
|
|
430
|
+
count: 500, // Use maximum for efficiency
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const response = await plaidClient.transactionsSync(request);
|
|
434
|
+
|
|
435
|
+
allTransactions = allTransactions.concat(response.data.added);
|
|
436
|
+
cursor = response.data.next_cursor;
|
|
437
|
+
hasMore = response.data.has_more;
|
|
438
|
+
|
|
439
|
+
console.log(`Fetched ${response.data.added.length} transactions`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
console.log(`Total transactions: ${allTransactions.length}`);
|
|
443
|
+
return allTransactions;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
console.error('Error getting all transactions:', error);
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
#### Transactions Get (Legacy)
|
|
452
|
+
|
|
453
|
+
For retrieving transactions within a specific date range:
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
async function getTransactions(
|
|
457
|
+
accessToken: string,
|
|
458
|
+
startDate: string,
|
|
459
|
+
endDate: string
|
|
460
|
+
) {
|
|
461
|
+
try {
|
|
462
|
+
const request = {
|
|
463
|
+
access_token: accessToken,
|
|
464
|
+
start_date: startDate, // Format: 'YYYY-MM-DD'
|
|
465
|
+
end_date: endDate, // Format: 'YYYY-MM-DD'
|
|
466
|
+
options: {
|
|
467
|
+
count: 250,
|
|
468
|
+
offset: 0,
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const response = await plaidClient.transactionsGet(request);
|
|
473
|
+
const transactions = response.data.transactions;
|
|
474
|
+
const totalTransactions = response.data.total_transactions;
|
|
475
|
+
|
|
476
|
+
console.log(`Retrieved ${transactions.length} of ${totalTransactions}`);
|
|
477
|
+
|
|
478
|
+
return transactions;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
console.error('Error getting transactions:', error);
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Paginated Transactions Get:**
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
async function getAllTransactionsInRange(
|
|
490
|
+
accessToken: string,
|
|
491
|
+
startDate: string,
|
|
492
|
+
endDate: string
|
|
493
|
+
) {
|
|
494
|
+
let offset = 0;
|
|
495
|
+
const batchSize = 500;
|
|
496
|
+
let allTransactions: any[] = [];
|
|
497
|
+
let totalTransactions = 0;
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
do {
|
|
501
|
+
const request = {
|
|
502
|
+
access_token: accessToken,
|
|
503
|
+
start_date: startDate,
|
|
504
|
+
end_date: endDate,
|
|
505
|
+
options: {
|
|
506
|
+
count: batchSize,
|
|
507
|
+
offset: offset,
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const response = await plaidClient.transactionsGet(request);
|
|
512
|
+
const transactions = response.data.transactions;
|
|
513
|
+
totalTransactions = response.data.total_transactions;
|
|
514
|
+
|
|
515
|
+
allTransactions = allTransactions.concat(transactions);
|
|
516
|
+
offset += transactions.length;
|
|
517
|
+
|
|
518
|
+
console.log(`Fetched ${allTransactions.length} of ${totalTransactions}`);
|
|
519
|
+
} while (allTransactions.length < totalTransactions);
|
|
520
|
+
|
|
521
|
+
return allTransactions;
|
|
522
|
+
} catch (error) {
|
|
523
|
+
console.error('Error getting all transactions:', error);
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Investments - Holdings and Transactions
|
|
530
|
+
|
|
531
|
+
Retrieve investment account holdings and transactions:
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
async function getInvestmentHoldings(accessToken: string) {
|
|
535
|
+
try {
|
|
536
|
+
const request = {
|
|
537
|
+
access_token: accessToken,
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const response = await plaidClient.investmentsHoldingsGet(request);
|
|
541
|
+
const holdings = response.data.holdings;
|
|
542
|
+
const securities = response.data.securities;
|
|
543
|
+
|
|
544
|
+
holdings.forEach(holding => {
|
|
545
|
+
const security = securities.find(s => s.security_id === holding.security_id);
|
|
546
|
+
console.log('Security:', security?.name);
|
|
547
|
+
console.log('Ticker:', security?.ticker_symbol);
|
|
548
|
+
console.log('Quantity:', holding.quantity);
|
|
549
|
+
console.log('Institution Price:', holding.institution_price);
|
|
550
|
+
console.log('Value:', holding.institution_value);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
return { holdings, securities };
|
|
554
|
+
} catch (error) {
|
|
555
|
+
console.error('Error getting investment holdings:', error);
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**Investment Transactions:**
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
async function getInvestmentTransactions(
|
|
565
|
+
accessToken: string,
|
|
566
|
+
startDate: string,
|
|
567
|
+
endDate: string
|
|
568
|
+
) {
|
|
569
|
+
try {
|
|
570
|
+
const request = {
|
|
571
|
+
access_token: accessToken,
|
|
572
|
+
start_date: startDate,
|
|
573
|
+
end_date: endDate,
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const response = await plaidClient.investmentsTransactionsGet(request);
|
|
577
|
+
const transactions = response.data.investment_transactions;
|
|
578
|
+
|
|
579
|
+
transactions.forEach(transaction => {
|
|
580
|
+
console.log('Date:', transaction.date);
|
|
581
|
+
console.log('Name:', transaction.name);
|
|
582
|
+
console.log('Type:', transaction.type);
|
|
583
|
+
console.log('Amount:', transaction.amount);
|
|
584
|
+
console.log('Quantity:', transaction.quantity);
|
|
585
|
+
console.log('Price:', transaction.price);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
return transactions;
|
|
589
|
+
} catch (error) {
|
|
590
|
+
console.error('Error getting investment transactions:', error);
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Liabilities - Loan and Credit Card Data
|
|
597
|
+
|
|
598
|
+
Access loan balances, interest rates, and credit card information:
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
async function getLiabilities(accessToken: string) {
|
|
602
|
+
try {
|
|
603
|
+
const request = {
|
|
604
|
+
access_token: accessToken,
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const response = await plaidClient.liabilitiesGet(request);
|
|
608
|
+
const liabilities = response.data.liabilities;
|
|
609
|
+
|
|
610
|
+
// Credit cards
|
|
611
|
+
liabilities.credit?.forEach(card => {
|
|
612
|
+
console.log('Credit Card:', card.name);
|
|
613
|
+
console.log('APRs:', card.aprs);
|
|
614
|
+
console.log('Last Payment:', card.last_payment_amount);
|
|
615
|
+
console.log('Minimum Payment:', card.minimum_payment_amount);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Student loans
|
|
619
|
+
liabilities.student?.forEach(loan => {
|
|
620
|
+
console.log('Student Loan:', loan.account_id);
|
|
621
|
+
console.log('Interest Rate:', loan.interest_rate_percentage);
|
|
622
|
+
console.log('Origination Date:', loan.origination_date);
|
|
623
|
+
console.log('Outstanding Balance:', loan.outstanding_interest_amount);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// Mortgages
|
|
627
|
+
liabilities.mortgage?.forEach(mortgage => {
|
|
628
|
+
console.log('Mortgage:', mortgage.account_id);
|
|
629
|
+
console.log('Interest Rate:', mortgage.interest_rate.percentage);
|
|
630
|
+
console.log('Origination Date:', mortgage.origination_date);
|
|
631
|
+
console.log('Maturity Date:', mortgage.maturity_date);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
return liabilities;
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.error('Error getting liabilities:', error);
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Payment Initiation (UK and Europe)
|
|
643
|
+
|
|
644
|
+
Create and manage payments:
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
import { PaymentInitiationPaymentCreateRequest } from 'plaid';
|
|
648
|
+
|
|
649
|
+
async function createPayment(accessToken: string) {
|
|
650
|
+
try {
|
|
651
|
+
const request: PaymentInitiationPaymentCreateRequest = {
|
|
652
|
+
recipient_id: 'recipient_id_from_previous_call',
|
|
653
|
+
reference: 'Invoice #12345',
|
|
654
|
+
amount: {
|
|
655
|
+
currency: 'GBP',
|
|
656
|
+
value: 100.00,
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const response = await plaidClient.paymentInitiationPaymentCreate(request);
|
|
661
|
+
const paymentId = response.data.payment_id;
|
|
662
|
+
|
|
663
|
+
console.log('Payment ID:', paymentId);
|
|
664
|
+
return paymentId;
|
|
665
|
+
} catch (error) {
|
|
666
|
+
console.error('Error creating payment:', error);
|
|
667
|
+
throw error;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**Get Payment Status:**
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
async function getPaymentStatus(paymentId: string) {
|
|
676
|
+
try {
|
|
677
|
+
const request = {
|
|
678
|
+
payment_id: paymentId,
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const response = await plaidClient.paymentInitiationPaymentGet(request);
|
|
682
|
+
const payment = response.data;
|
|
683
|
+
|
|
684
|
+
console.log('Status:', payment.status);
|
|
685
|
+
console.log('Amount:', payment.amount);
|
|
686
|
+
console.log('Last Updated:', payment.last_status_update);
|
|
687
|
+
|
|
688
|
+
return payment;
|
|
689
|
+
} catch (error) {
|
|
690
|
+
console.error('Error getting payment status:', error);
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
## Items Management
|
|
697
|
+
|
|
698
|
+
An Item represents a user's connection to a financial institution.
|
|
699
|
+
|
|
700
|
+
### Get Item Information
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
async function getItem(accessToken: string) {
|
|
704
|
+
try {
|
|
705
|
+
const request = {
|
|
706
|
+
access_token: accessToken,
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const response = await plaidClient.itemGet(request);
|
|
710
|
+
const item = response.data.item;
|
|
711
|
+
|
|
712
|
+
console.log('Item ID:', item.item_id);
|
|
713
|
+
console.log('Institution ID:', item.institution_id);
|
|
714
|
+
console.log('Available Products:', item.available_products);
|
|
715
|
+
console.log('Billed Products:', item.billed_products);
|
|
716
|
+
console.log('Error:', item.error);
|
|
717
|
+
|
|
718
|
+
return item;
|
|
719
|
+
} catch (error) {
|
|
720
|
+
console.error('Error getting item:', error);
|
|
721
|
+
throw error;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Remove Item
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
async function removeItem(accessToken: string) {
|
|
730
|
+
try {
|
|
731
|
+
const request = {
|
|
732
|
+
access_token: accessToken,
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const response = await plaidClient.itemRemove(request);
|
|
736
|
+
console.log('Item removed successfully');
|
|
737
|
+
return response.data;
|
|
738
|
+
} catch (error) {
|
|
739
|
+
console.error('Error removing item:', error);
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### Update Item Webhook
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
async function updateWebhook(accessToken: string, newWebhook: string) {
|
|
749
|
+
try {
|
|
750
|
+
const request = {
|
|
751
|
+
access_token: accessToken,
|
|
752
|
+
webhook: newWebhook,
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
const response = await plaidClient.itemWebhookUpdate(request);
|
|
756
|
+
const item = response.data.item;
|
|
757
|
+
|
|
758
|
+
console.log('Webhook updated to:', newWebhook);
|
|
759
|
+
return item;
|
|
760
|
+
} catch (error) {
|
|
761
|
+
console.error('Error updating webhook:', error);
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
## Webhooks
|
|
768
|
+
|
|
769
|
+
Plaid sends webhook notifications for various events. Configure webhooks via `/link/token/create` or the Plaid Dashboard.
|
|
770
|
+
|
|
771
|
+
### Webhook Handler Example
|
|
772
|
+
|
|
773
|
+
```typescript
|
|
774
|
+
import express from 'express';
|
|
775
|
+
|
|
776
|
+
const app = express();
|
|
777
|
+
app.use(express.json());
|
|
778
|
+
|
|
779
|
+
app.post('/plaid/webhook', async (req, res) => {
|
|
780
|
+
const webhook = req.body;
|
|
781
|
+
|
|
782
|
+
console.log('Webhook Type:', webhook.webhook_type);
|
|
783
|
+
console.log('Webhook Code:', webhook.webhook_code);
|
|
784
|
+
|
|
785
|
+
switch (webhook.webhook_type) {
|
|
786
|
+
case 'TRANSACTIONS':
|
|
787
|
+
await handleTransactionsWebhook(webhook);
|
|
788
|
+
break;
|
|
789
|
+
case 'ITEM':
|
|
790
|
+
await handleItemWebhook(webhook);
|
|
791
|
+
break;
|
|
792
|
+
case 'AUTH':
|
|
793
|
+
await handleAuthWebhook(webhook);
|
|
794
|
+
break;
|
|
795
|
+
default:
|
|
796
|
+
console.log('Unknown webhook type:', webhook.webhook_type);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
res.json({ status: 'received' });
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
async function handleTransactionsWebhook(webhook: any) {
|
|
803
|
+
if (webhook.webhook_code === 'SYNC_UPDATES_AVAILABLE') {
|
|
804
|
+
const itemId = webhook.item_id;
|
|
805
|
+
console.log('New transactions available for item:', itemId);
|
|
806
|
+
// Fetch new transactions using transactionsSync
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function handleItemWebhook(webhook: any) {
|
|
811
|
+
if (webhook.webhook_code === 'ERROR') {
|
|
812
|
+
console.log('Item error:', webhook.error);
|
|
813
|
+
// Handle item error
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function handleAuthWebhook(webhook: any) {
|
|
818
|
+
if (webhook.webhook_code === 'AUTOMATICALLY_VERIFIED') {
|
|
819
|
+
console.log('Account automatically verified:', webhook.account_id);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
## Sandbox Testing
|
|
825
|
+
|
|
826
|
+
The Sandbox environment provides test data and utilities for development.
|
|
827
|
+
|
|
828
|
+
### Fire a Test Webhook
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
async function fireSandboxWebhook(accessToken: string) {
|
|
832
|
+
try {
|
|
833
|
+
const request = {
|
|
834
|
+
access_token: accessToken,
|
|
835
|
+
webhook_code: 'SYNC_UPDATES_AVAILABLE',
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
const response = await plaidClient.sandboxItemFireWebhook(request);
|
|
839
|
+
console.log('Webhook fired:', response.data);
|
|
840
|
+
return response.data;
|
|
841
|
+
} catch (error) {
|
|
842
|
+
console.error('Error firing webhook:', error);
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### Reset Sandbox Item Login
|
|
849
|
+
|
|
850
|
+
```typescript
|
|
851
|
+
async function resetSandboxItem(accessToken: string) {
|
|
852
|
+
try {
|
|
853
|
+
const request = {
|
|
854
|
+
access_token: accessToken,
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
const response = await plaidClient.sandboxItemResetLogin(request);
|
|
858
|
+
console.log('Item login reset');
|
|
859
|
+
return response.data;
|
|
860
|
+
} catch (error) {
|
|
861
|
+
console.error('Error resetting item:', error);
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### Set Verification Status (Sandbox)
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
async function setVerificationStatus(
|
|
871
|
+
accessToken: string,
|
|
872
|
+
accountId: string,
|
|
873
|
+
verificationStatus: string
|
|
874
|
+
) {
|
|
875
|
+
try {
|
|
876
|
+
const request = {
|
|
877
|
+
access_token: accessToken,
|
|
878
|
+
account_id: accountId,
|
|
879
|
+
verification_status: verificationStatus,
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const response = await plaidClient.sandboxItemSetVerificationStatus(request);
|
|
883
|
+
console.log('Verification status set');
|
|
884
|
+
return response.data;
|
|
885
|
+
} catch (error) {
|
|
886
|
+
console.error('Error setting verification status:', error);
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
## Error Handling
|
|
893
|
+
|
|
894
|
+
Plaid errors include an `error_type`, `error_code`, and HTTP status code.
|
|
895
|
+
|
|
896
|
+
```typescript
|
|
897
|
+
import { PlaidError } from 'plaid';
|
|
898
|
+
|
|
899
|
+
async function makeApiCall(accessToken: string) {
|
|
900
|
+
try {
|
|
901
|
+
const response = await plaidClient.accountsGet({
|
|
902
|
+
access_token: accessToken,
|
|
903
|
+
});
|
|
904
|
+
return response.data;
|
|
905
|
+
} catch (error: any) {
|
|
906
|
+
if (error.response) {
|
|
907
|
+
const plaidError = error.response.data;
|
|
908
|
+
|
|
909
|
+
console.log('Error Type:', plaidError.error_type);
|
|
910
|
+
console.log('Error Code:', plaidError.error_code);
|
|
911
|
+
console.log('Error Message:', plaidError.error_message);
|
|
912
|
+
console.log('Display Message:', plaidError.display_message);
|
|
913
|
+
console.log('HTTP Status:', error.response.status);
|
|
914
|
+
|
|
915
|
+
switch (plaidError.error_type) {
|
|
916
|
+
case 'ITEM_ERROR':
|
|
917
|
+
if (plaidError.error_code === 'ITEM_LOGIN_REQUIRED') {
|
|
918
|
+
console.log('User needs to re-authenticate');
|
|
919
|
+
// Trigger Link update mode
|
|
920
|
+
}
|
|
921
|
+
break;
|
|
922
|
+
case 'RATE_LIMIT_EXCEEDED':
|
|
923
|
+
console.log('Rate limit exceeded, retry after delay');
|
|
924
|
+
// Implement exponential backoff
|
|
925
|
+
break;
|
|
926
|
+
case 'API_ERROR':
|
|
927
|
+
console.log('Plaid API error, retry request');
|
|
928
|
+
// Retry with idempotency key if available
|
|
929
|
+
break;
|
|
930
|
+
case 'INVALID_REQUEST':
|
|
931
|
+
console.log('Invalid request parameters');
|
|
932
|
+
break;
|
|
933
|
+
case 'INVALID_INPUT':
|
|
934
|
+
console.log('Invalid input data');
|
|
935
|
+
break;
|
|
936
|
+
case 'INSTITUTION_ERROR':
|
|
937
|
+
console.log('Institution is down or experiencing issues');
|
|
938
|
+
break;
|
|
939
|
+
default:
|
|
940
|
+
console.log('Unexpected error type');
|
|
941
|
+
}
|
|
942
|
+
} else {
|
|
943
|
+
console.error('Network or unexpected error:', error);
|
|
944
|
+
}
|
|
945
|
+
throw error;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
### Retry Logic with Exponential Backoff
|
|
951
|
+
|
|
952
|
+
```typescript
|
|
953
|
+
async function makeApiCallWithRetry<T>(
|
|
954
|
+
apiCall: () => Promise<T>,
|
|
955
|
+
maxRetries = 3
|
|
956
|
+
): Promise<T> {
|
|
957
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
958
|
+
try {
|
|
959
|
+
return await apiCall();
|
|
960
|
+
} catch (error: any) {
|
|
961
|
+
const isLastAttempt = attempt === maxRetries;
|
|
962
|
+
const shouldRetry = error.response?.data?.error_type === 'RATE_LIMIT_EXCEEDED' ||
|
|
963
|
+
error.response?.data?.error_type === 'API_ERROR';
|
|
964
|
+
|
|
965
|
+
if (!shouldRetry || isLastAttempt) {
|
|
966
|
+
throw error;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
970
|
+
console.log(`Retrying after ${delay}ms (attempt ${attempt}/${maxRetries})`);
|
|
971
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
throw new Error('Max retries exceeded');
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Usage
|
|
978
|
+
const accounts = await makeApiCallWithRetry(() =>
|
|
979
|
+
plaidClient.accountsGet({ access_token: accessToken })
|
|
980
|
+
);
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
## Rate Limits
|
|
984
|
+
|
|
985
|
+
Plaid enforces rate limits to ensure API stability:
|
|
986
|
+
|
|
987
|
+
- `/auth/get`: 15 requests per Item per minute (Production)
|
|
988
|
+
- `/institutions/get`: 25 requests per client per minute (Production), 10 requests per client per minute (Sandbox)
|
|
989
|
+
- Most other endpoints: Custom limits based on your account
|
|
990
|
+
|
|
991
|
+
To reduce rate limit errors:
|
|
992
|
+
|
|
993
|
+
- Increase the `count` parameter in `/transactions/sync` to the maximum of 500
|
|
994
|
+
- Cache responses when appropriate
|
|
995
|
+
- Implement exponential backoff retry logic
|
|
996
|
+
- Use webhooks instead of polling for updates
|
|
997
|
+
|
|
998
|
+
```typescript
|
|
999
|
+
// Efficient transactions sync with maximum count
|
|
1000
|
+
async function efficientTransactionSync(accessToken: string, cursor?: string) {
|
|
1001
|
+
const request = {
|
|
1002
|
+
access_token: accessToken,
|
|
1003
|
+
cursor: cursor,
|
|
1004
|
+
count: 500, // Use maximum to reduce number of requests
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
const response = await plaidClient.transactionsSync(request);
|
|
1008
|
+
return response.data;
|
|
1009
|
+
}
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
## Complete Integration Example
|
|
1013
|
+
|
|
1014
|
+
```typescript
|
|
1015
|
+
import express from 'express';
|
|
1016
|
+
import { Configuration, PlaidApi, PlaidEnvironments, Products, CountryCode } from 'plaid';
|
|
1017
|
+
import dotenv from 'dotenv';
|
|
1018
|
+
|
|
1019
|
+
dotenv.config();
|
|
1020
|
+
|
|
1021
|
+
const app = express();
|
|
1022
|
+
app.use(express.json());
|
|
1023
|
+
|
|
1024
|
+
// Initialize Plaid client
|
|
1025
|
+
const configuration = new Configuration({
|
|
1026
|
+
basePath: PlaidEnvironments.sandbox,
|
|
1027
|
+
baseOptions: {
|
|
1028
|
+
headers: {
|
|
1029
|
+
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
|
|
1030
|
+
'PLAID-SECRET': process.env.PLAID_SANDBOX_SECRET,
|
|
1031
|
+
'Plaid-Version': '2020-09-14',
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
const plaidClient = new PlaidApi(configuration);
|
|
1037
|
+
|
|
1038
|
+
// Store access tokens (use a database in production)
|
|
1039
|
+
const accessTokenStore = new Map<string, string>();
|
|
1040
|
+
|
|
1041
|
+
// Create link token
|
|
1042
|
+
app.post('/api/create_link_token', async (req, res) => {
|
|
1043
|
+
try {
|
|
1044
|
+
const { userId } = req.body;
|
|
1045
|
+
|
|
1046
|
+
const response = await plaidClient.linkTokenCreate({
|
|
1047
|
+
user: {
|
|
1048
|
+
client_user_id: userId,
|
|
1049
|
+
},
|
|
1050
|
+
client_name: 'My Financial App',
|
|
1051
|
+
products: [Products.Auth, Products.Transactions],
|
|
1052
|
+
country_codes: [CountryCode.Us],
|
|
1053
|
+
language: 'en',
|
|
1054
|
+
webhook: 'https://your-domain.com/plaid/webhook',
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
res.json({ link_token: response.data.link_token });
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
console.error('Error creating link token:', error);
|
|
1060
|
+
res.status(500).json({ error: 'Failed to create link token' });
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// Exchange public token
|
|
1065
|
+
app.post('/api/exchange_public_token', async (req, res) => {
|
|
1066
|
+
try {
|
|
1067
|
+
const { public_token, userId } = req.body;
|
|
1068
|
+
|
|
1069
|
+
const response = await plaidClient.itemPublicTokenExchange({
|
|
1070
|
+
public_token: public_token,
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
const accessToken = response.data.access_token;
|
|
1074
|
+
const itemId = response.data.item_id;
|
|
1075
|
+
|
|
1076
|
+
// Store access token securely (use database in production)
|
|
1077
|
+
accessTokenStore.set(userId, accessToken);
|
|
1078
|
+
|
|
1079
|
+
res.json({ success: true, item_id: itemId });
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
console.error('Error exchanging public token:', error);
|
|
1082
|
+
res.status(500).json({ error: 'Failed to exchange public token' });
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// Get accounts
|
|
1087
|
+
app.get('/api/accounts/:userId', async (req, res) => {
|
|
1088
|
+
try {
|
|
1089
|
+
const { userId } = req.params;
|
|
1090
|
+
const accessToken = accessTokenStore.get(userId);
|
|
1091
|
+
|
|
1092
|
+
if (!accessToken) {
|
|
1093
|
+
return res.status(404).json({ error: 'No access token found' });
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const response = await plaidClient.accountsGet({
|
|
1097
|
+
access_token: accessToken,
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
res.json({ accounts: response.data.accounts });
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
console.error('Error getting accounts:', error);
|
|
1103
|
+
res.status(500).json({ error: 'Failed to get accounts' });
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// Get transactions
|
|
1108
|
+
app.get('/api/transactions/:userId', async (req, res) => {
|
|
1109
|
+
try {
|
|
1110
|
+
const { userId } = req.params;
|
|
1111
|
+
const accessToken = accessTokenStore.get(userId);
|
|
1112
|
+
|
|
1113
|
+
if (!accessToken) {
|
|
1114
|
+
return res.status(404).json({ error: 'No access token found' });
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const response = await plaidClient.transactionsSync({
|
|
1118
|
+
access_token: accessToken,
|
|
1119
|
+
count: 100,
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
res.json({
|
|
1123
|
+
added: response.data.added,
|
|
1124
|
+
modified: response.data.modified,
|
|
1125
|
+
removed: response.data.removed,
|
|
1126
|
+
next_cursor: response.data.next_cursor,
|
|
1127
|
+
has_more: response.data.has_more,
|
|
1128
|
+
});
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
console.error('Error getting transactions:', error);
|
|
1131
|
+
res.status(500).json({ error: 'Failed to get transactions' });
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// Webhook handler
|
|
1136
|
+
app.post('/plaid/webhook', async (req, res) => {
|
|
1137
|
+
const webhook = req.body;
|
|
1138
|
+
console.log('Received webhook:', webhook);
|
|
1139
|
+
|
|
1140
|
+
if (webhook.webhook_type === 'TRANSACTIONS') {
|
|
1141
|
+
if (webhook.webhook_code === 'SYNC_UPDATES_AVAILABLE') {
|
|
1142
|
+
// Fetch new transactions
|
|
1143
|
+
console.log('New transactions available for item:', webhook.item_id);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
res.json({ status: 'received' });
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
const PORT = process.env.PORT || 3000;
|
|
1151
|
+
app.listen(PORT, () => {
|
|
1152
|
+
console.log(`Server running on port ${PORT}`);
|
|
1153
|
+
});
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
## Useful Links
|
|
1157
|
+
|
|
1158
|
+
- **Documentation:** https://plaid.com/docs/
|
|
1159
|
+
- **API Reference:** https://plaid.com/docs/api/
|
|
1160
|
+
- **Dashboard:** https://dashboard.plaid.com/
|
|
1161
|
+
- **GitHub Repository:** https://github.com/plaid/plaid-node
|
|
1162
|
+
- **Quickstart Guide:** https://plaid.com/docs/quickstart/
|
|
1163
|
+
- **Changelog:** https://plaid.com/docs/changelog/
|