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,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/
|