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