├── .dockerignore ├── .env.example ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── assets └── quickstart.jpeg ├── docker-compose.yml ├── frontend ├── .gitignore ├── .npmrc ├── .nvmrc ├── Dockerfile ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── App.module.scss │ ├── App.tsx │ ├── Components │ │ ├── Endpoint │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Error │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Headers │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Link │ │ │ └── index.tsx │ │ ├── ProductTypes │ │ │ ├── Items.tsx │ │ │ ├── ProductTypesContainer │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ └── Products.tsx │ │ └── Table │ │ │ ├── Identity.module.scss │ │ │ ├── Identity.tsx │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ ├── Context │ │ └── index.tsx │ ├── dataUtilities.ts │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── setupProxy.js │ └── setupTests.ts └── tsconfig.json ├── go ├── .env ├── .env.example ├── Dockerfile ├── go.mod ├── go.sum ├── server.go └── start.sh ├── java ├── .env ├── .env.example ├── .gitignore ├── Dockerfile ├── config.yml ├── pom.xml ├── src │ └── main │ │ └── java │ │ └── com │ │ └── plaid │ │ └── quickstart │ │ ├── QuickstartApplication.java │ │ ├── QuickstartConfiguration.java │ │ └── resources │ │ ├── AccessTokenResource.java │ │ ├── AccountsResource.java │ │ ├── AssetsResource.java │ │ ├── AuthResource.java │ │ ├── BalanceResource.java │ │ ├── CraResource.java │ │ ├── HoldingsResource.java │ │ ├── IdentityResource.java │ │ ├── InfoResource.java │ │ ├── InvestmentTransactionsResource.java │ │ ├── ItemResource.java │ │ ├── LinkTokenResource.java │ │ ├── LinkTokenWithPaymentResource.java │ │ ├── PaymentInitiationResource.java │ │ ├── PublicTokenResource.java │ │ ├── SignalResource.java │ │ ├── StatementsResource.java │ │ ├── TransactionsResource.java │ │ ├── TransferAuthorizeResource.java │ │ ├── TransferCreateResource.java │ │ └── UserTokenResource.java └── start.sh ├── node ├── .dockerignore ├── .env ├── .env.example ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── Dockerfile ├── index.js ├── package-lock.json ├── package.json └── start.sh ├── python ├── .env ├── .env.example ├── Dockerfile ├── requirements.txt ├── server.py └── start.sh └── ruby ├── .env ├── .env.example ├── .gitignore ├── .ruby-version ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── app.rb └── start.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Get your Plaid API keys from the dashboard: https://dashboard.plaid.com/team/keys 2 | PLAID_CLIENT_ID= 3 | PLAID_SECRET= 4 | 5 | # Use 'sandbox' to test with fake credentials in Plaid's Sandbox environment 6 | # Use 'production' to use real data 7 | # NOTE: Some major US institutions (including Chase, Wells Fargo, Bank of America) won't work unless you have been approved for full production. 8 | # To test these institutions with live data, get full production approval first at https://dashboard.plaid.com/overview/production 9 | # Once approved, set your environment to 'production' to test. 10 | # NOTE: To use Production, you must set a use case for Link. 11 | # You can do this in the Dashboard under Link -> Link Customization -> Data Transparency: 12 | # https://dashboard.plaid.com/link/data-transparency-v5 13 | PLAID_ENV=sandbox 14 | 15 | # PLAID_PRODUCTS is a comma-separated list of products to use when 16 | # initializing Link, e.g. PLAID_PRODUCTS=auth,transactions. 17 | # see https://plaid.com/docs/api/link/#link-token-create-request-products for a complete list. 18 | # Only institutions that support ALL listed products will work. 19 | # If you don't see the institution you want in Link, or get a "Connectivity not supported" error, 20 | # Remove any products you aren't using. 21 | # NOTE: The Identity Verification (IDV) and Income APIs have separate Quickstart apps. 22 | # For IDV, use https://github.com/plaid/idv-quickstart 23 | # For Income, use https://github.com/plaid/income-sample 24 | # Important: 25 | # When moving to Production, make sure to update this list with only the products 26 | # you plan to use. Otherwise, you may be billed for unneeded products. 27 | PLAID_PRODUCTS=auth,transactions 28 | 29 | # PLAID_COUNTRY_CODES is a comma-separated list of countries to use when 30 | # initializing Link, e.g. PLAID_COUNTRY_CODES=US,CA. 31 | # Institutions from all listed countries will be shown. If Link is launched with multiple country codes, 32 | # only products that you are enabled for in all countries will be used by Link. 33 | # See https://plaid.com/docs/api/link/#link-token-create-request-country-codes for a complete list 34 | PLAID_COUNTRY_CODES=US,CA 35 | 36 | # PLAID_REDIRECT_URI is optional for this Quickstart application. 37 | # If you're not sure if you need to use this field, you can leave it blank 38 | # 39 | # If using this field on Sandbox, set PLAID_REDIRECT_URI to http://localhost:3000/ (no quote characters) 40 | # The OAuth redirect flow requires an endpoint on the developer's website 41 | # that the bank website should redirect to. You will need to configure 42 | # this redirect URI for your client ID through the Plaid developer dashboard 43 | # at https://dashboard.plaid.com/team/api. 44 | # For development or production, you will need to use an https:// url 45 | # Instructions to create a self-signed certificate for localhost can be found at https://github.com/plaid/quickstart/blob/master/README.md#testing-oauth 46 | PLAID_REDIRECT_URI= 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.swp 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # go 41 | go/quickstart 42 | 43 | # macOS 44 | **/.DS_Store 45 | 46 | # docker-compose 47 | docker-compose.local*.yml 48 | 49 | *.iml 50 | .env 51 | .idea 52 | 53 | #vscode 54 | .vscode 55 | 56 | #Python virtual nv 57 | python/venv/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Plaid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_COMPOSE := docker compose 2 | DOCKER_COMPOSE_YML := --file docker-compose.yml 3 | ifneq ("$(wildcard docker-compose.local.yml)","") 4 | DOCKER_COMPOSE_YML += --file docker-compose.local.yml 5 | endif 6 | 7 | language := node 8 | SUCCESS_MESSAGE := "✅ $(language) quickstart is running on http://localhost:3000" 9 | 10 | .PHONY: up 11 | up: 12 | REACT_APP_API_HOST=http://$(language):8000 \ 13 | $(DOCKER_COMPOSE) \ 14 | $(DOCKER_COMPOSE_YML) \ 15 | $@ --build --detach --remove-orphans \ 16 | $(language) 17 | @echo $(SUCCESS_MESSAGE) 18 | 19 | .PHONY: logs 20 | logs: 21 | $(DOCKER_COMPOSE) \ 22 | $@ --follow \ 23 | $(language) frontend 24 | 25 | .PHONY: stop build 26 | stop build: 27 | $(DOCKER_COMPOSE) \ 28 | $(DOCKER_COMPOSE_YML) \ 29 | $@ \ 30 | $(language) frontend 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plaid quickstart 2 | 3 | This repository accompanies Plaid's [**quickstart guide**][quickstart]. 4 | 5 | Here you'll find full example integration apps using our [**client libraries**][libraries]. 6 | 7 | This Quickstart is designed to show as many products and configurations as possible, including all five officially supported client libraries and multiple Plaid APIs, against a React frontend. 8 | 9 | If you prefer a non-React frontend platform, or a more minimal backend in one language with one endpoint, see the [Tiny Quickstart](https://github.com/plaid/tiny-quickstart), which shows a simpler backend and is available for JavaScript, Next.js, React, and React Native frontends. 10 | 11 | For Identity Verification, see the [Identity Verification Quickstart](https://github.com/plaid/idv-quickstart). 12 | 13 | For Income, see the [Income sample app](https://github.com/plaid/income-sample). 14 | 15 | For a more in-depth Transfer Quickstart, see the [Transfer Quickstart](https://github.com/plaid/transfer-quickstart) (Node only). 16 | 17 | ![Plaid quickstart app](/assets/quickstart.jpeg) 18 | 19 | ## Table of contents 20 | 21 | 22 | 23 | - [1. Clone the repository](#1-clone-the-repository) 24 | - [Special instructions for Windows](#special-instructions-for-windows) 25 | - [2. Set up your environment variables](#2-set-up-your-environment-variables) 26 | - [3. Run the quickstart](#3-run-the-quickstart) 27 | - [Run without Docker](#run-without-docker) 28 | - [Pre-requisites](#pre-requisites) 29 | - [1. Running the backend](#1-running-the-backend) 30 | - [Node](#node) 31 | - [Python](#python) 32 | - [Ruby](#ruby) 33 | - [Go](#go) 34 | - [Java](#java) 35 | - [.NET](#net) (community support only) 36 | - [2. Running the frontend](#2-running-the-frontend) 37 | - [Run with Docker](#run-with-docker) 38 | - [Pre-requisites](#pre-requisites-1) 39 | - [Running](#running-1) 40 | - [Start the container](#start-the-container) 41 | - [View the logs](#view-the-logs) 42 | - [Stop the container](#stop-the-container) 43 | - [Test credentials](#test-credentials) 44 | - [Troubleshooting](#troubleshooting) 45 | - [Testing OAuth](#testing-oauth) 46 | 47 | 48 | 49 | ## 1. Clone the repository 50 | 51 | Using https: 52 | 53 | ```bash 54 | git clone https://github.com/plaid/quickstart 55 | cd quickstart 56 | ``` 57 | 58 | Alternatively, if you use ssh: 59 | 60 | ```bash 61 | git clone git@github.com:plaid/quickstart.git 62 | cd quickstart 63 | ``` 64 | 65 | #### Special instructions for Windows 66 | 67 | Note - because this repository makes use of symbolic links, to run this on a Windows machine, make sure you have checked the "enable symbolic links" box when you download Git to your local machine. Then you can run the above commands to clone the quickstart. Otherwise, you may open your Git Bash terminal as an administrator and use the following command when cloning the project 68 | 69 | ```bash 70 | git clone -c core.symlinks=true https://github.com/plaid/quickstart 71 | ``` 72 | 73 | ## 2. Set up your environment variables 74 | 75 | ```bash 76 | cp .env.example .env 77 | ``` 78 | 79 | Copy `.env.example` to a new file called `.env` and fill out the environment variables inside. At 80 | minimum `PLAID_CLIENT_ID` and `PLAID_SECRET` must be filled out. Get your Client ID and secrets from 81 | the dashboard: [https://dashboard.plaid.com/developers/keys](https://dashboard.plaid.com/developers/keys) 82 | 83 | > NOTE: `.env` files are a convenient local development tool. Never run a production application 84 | > using an environment file with secrets in it. 85 | 86 | ## 3. Run the Quickstart 87 | 88 | There are two ways to run the various language quickstarts in this repository. You can choose to run the 89 | code directly or you can run it in Docker. If you would like to run the code via Docker, skip to the 90 | [Run with Docker](#run-with-docker) section. 91 | 92 | ### Run without Docker 93 | 94 | #### Pre-requisites 95 | 96 | - The language you intend to use is installed on your machine and available at your command line. 97 | This repo should generally work with active LTS versions of each language such as node >= 14, 98 | python >= 3.8, ruby >= 2.6, etc. 99 | - Your environment variables populated in `.env` 100 | - [npm](https://www.npmjs.com/get-npm) 101 | - If using Windows, a command line utility capable of running basic Unix shell commands 102 | 103 | #### 1. Running the backend 104 | 105 | Once started with one of the commands below, the quickstart will be running on http://localhost:8000 for the backend. Enter the additional commands in step 2 to run the frontend which will run on http://localhost:3000. 106 | 107 | ##### Node 108 | 109 | ```bash 110 | $ cd ./node 111 | $ npm install 112 | $ ./start.sh 113 | ``` 114 | 115 | ##### Python 116 | 117 | **:warning: As `python2` has reached its end of life, only `python3` is supported.** 118 | 119 | ```bash 120 | cd ./python 121 | 122 | # If you use virtualenv 123 | # virtualenv venv 124 | # source venv/bin/activate 125 | 126 | pip3 install -r requirements.txt 127 | ./start.sh 128 | ``` 129 | 130 | If you get this error message: 131 | 132 | ```txt 133 | ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:749) 134 | ``` 135 | 136 | You may need to run the following command in your terminal for your particular version of python in order to install SSL certificates: 137 | 138 | ```bash 139 | # examples: 140 | open /Applications/Python\ 3.9/Install\ Certificates.command 141 | # or 142 | open /Applications/Python\ 3.6/Install\ Certificates.command 143 | ``` 144 | 145 | ##### Ruby 146 | 147 | ```bash 148 | cd ./ruby 149 | bundle 150 | ./start.sh 151 | ``` 152 | 153 | ##### Go 154 | 155 | ```bash 156 | cd ./go 157 | go build 158 | ./start.sh 159 | ``` 160 | 161 | ##### Java 162 | 163 | ```bash 164 | cd ./java 165 | mvn clean package 166 | ./start.sh 167 | ``` 168 | 169 | ##### .NET 170 | 171 | A community-supported implementation of the Plaid Quickstart using the [Going.Plaid](https://github.com/viceroypenguin/Going.Plaid) client library can be found at [PlaidQuickstartBlazor](https://github.com/jcoliz/PlaidQuickstartBlazor). Note that Plaid does not provide first-party support for .NET client libraries and that this Quickstart and client library are not created, reviewed, or supported by Plaid. 172 | 173 | #### 2. Running the frontend 174 | 175 | ```bash 176 | cd ./frontend 177 | npm ci 178 | npm start 179 | ``` 180 | 181 | ### Run with Docker 182 | 183 | #### Pre-requisites 184 | 185 | - `make` available at your command line 186 | - Docker installed and running on your machine: https://docs.docker.com/get-docker/ 187 | - Your environment variables populated in `.env` 188 | - If using Windows, a working Linux installation on Windows 10. If you are using Windows and do not already have WSL or Cygwin configured, we recommend [running without Docker](#run-without-docker). 189 | 190 | #### Running 191 | 192 | There are three basic `make` commands available 193 | 194 | - `up`: builds and starts the container 195 | - `logs`: tails logs 196 | - `stop`: stops the container 197 | 198 | Each of these should be used with a `language` argument, which is one of `node`, `python`, `ruby`, 199 | `java`, or `go`. If unspecified, the default is `node`. 200 | 201 | ##### Start the container 202 | 203 | ```bash 204 | make up language=node 205 | ``` 206 | 207 | The quickstart backend is now running on http://localhost:8000 and frontend on http://localhost:3000. 208 | 209 | If you make changes to one of the server files such as `index.js`, `server.go`, etc, or to the 210 | `.env` file, simply run `make up language=node` again to rebuild and restart the container. 211 | 212 | If you experience a Docker connection error when running the command above, try the following: 213 | 214 | - Make sure Docker is running 215 | - Try running the command prefixed with `sudo` 216 | 217 | ##### View the logs 218 | 219 | ```bash 220 | make logs language=node 221 | ``` 222 | 223 | ##### Stop the container 224 | 225 | ```bash 226 | make stop language=node 227 | ``` 228 | 229 | ## Test credentials 230 | 231 | In Sandbox, you can log in to any supported institution using `user_good` as the username and `pass_good` as the password. If prompted to enter a 2-factor authentication code, enter `1234`. In Production, use real-life credentials. 232 | 233 | ### Transactions test credentials 234 | For Transactions, you will get the most realistic results using `user_transactions_dynamic` as the username, and any non-blank string as the password. For more details on the special capabilities of this test user, see the [docs](https://plaid.com/docs/transactions/transactions-data/#testing-pending-and-posted-transactions). 235 | 236 | ### Credit test credentials 237 | For credit and underwriting products like Assets and Statements, you will get the most realistic results using one of the [credit and underwriting tests credentials](https://plaid.com/docs/sandbox/test-credentials/#credit-and-income-testing-credentials), like `user_bank_income` / `{}`. 238 | 239 | ## Troubleshooting 240 | 241 | ### Link fails in Production with "something went wrong" / `INVALID_SERVER_ERROR` but works in Sandbox 242 | 243 | If Link works in Sandbox but fails in Production, the error is most likely one of the following: 244 | 1) You need to set a use case for Link, which you can do in the Plaid Dashboard under [Link -> Customization -> Data Transparency Messaging](https://dashboard.plaid.com/link/data-transparency-v5). 245 | 2) You don't yet have OAuth access for the institution you selected. This is especially common if the institution is Chase or Charles Schwab, which have longer OAuth registration turnarounds. To check your OAuth registration status and see if you have any required action items, see the [US OAuth Institutions page](https://dashboard.plaid.com/settings/compliance/us-oauth-institutions) in the Dashboard. 246 | 247 | ### Can't get a link token, or API calls are 400ing 248 | 249 | View the server logs to see the associated error message with detailed troubleshooting instructions. If you can't view logs locally, view them via the [Dashboard activity logs](https://dashboard.plaid.com/activity/logs). 250 | 251 | ### Works only when `PLAID_REDIRECT_URI` is not specified 252 | Make sure to add the redirect URI to the Allowed Redirect URIs list in the [Plaid Dashboard](https://dashboard.plaid.com/team/api). 253 | 254 | ### "Connectivity not supported" 255 | 256 | If you get a "Connectivity not supported" error after selecting a financial institution in Link, you probably specified some products in your .env file that the target financial institution doesn't support. Remove the unsupported products and try again. 257 | 258 | ### "You need to update your app" or "institution not supported" 259 | 260 | If you get a "You need to update your app" or "institution not supported" error after selecting a financial institution in Link, you're probably running the Quickstart in Production and attempting to link an institution, such as Chase or Wells Fargo, that requires an OAuth-based connection. In order to make OAuth connections to US-based institutions in Production, you must have full Production access approval, and certain institutions may also require additional approvals before you can be enabled. To use this institution, [apply for full Production access](https://dashboard.plaid.com/overview/production) and see the [OAuth insitutions page](https://dashboard.plaid.com/team/oauth-institutions) for any other required steps and to track your OAuth enablement status. 261 | 262 | ### "oauth uri does not contain a valid oauth_state_id query parameter" 263 | 264 | If you get the console error "oauth uri does not contain a valid oauth_state_id query parameter", you are attempting to initialize Link with a redirect uri when it is not necessary to do so. The `receivedRedirectUri` should not be set when initializing Link for the first time. It is used when initializing Link for the second time, after returning from the OAuth redirect. 265 | 266 | ## Testing OAuth 267 | 268 | Some institutions require an OAuth redirect 269 | authentication flow, where the end user is redirected to the bank’s website or mobile app to 270 | authenticate. 271 | 272 | To test the OAuth flow in Sandbox, select any institution that uses an OAuth connection with Plaid (a partial list can be found on the [Dashboard OAuth Institutions page](https://dashboard.plaid.com/team/oauth-institutions)), or choose 'Platypus OAuth Bank' from the list of financial institutions in Plaid Link. 273 | 274 | ### Testing OAuth with a redirect URI (optional) 275 | 276 | To test the OAuth flow in Sandbox with a [redirect URI](https://www.plaid.com/docs/link/oauth/#create-and-register-a-redirect-uri), you should set `PLAID_REDIRECT_URI=http://localhost:3000/` in `.env`. You will also need to register this localhost redirect URI in the 277 | [Plaid dashboard under Developers > API > Allowed redirect URIs][dashboard-api-section]. It is not required to configure a redirect URI in the .env file to use OAuth with the Quickstart. 278 | 279 | #### Instructions for using https with localhost 280 | 281 | If you want to test OAuth in development with a redirect URI, you need to use https and set `PLAID_REDIRECT_URI=https://localhost:3000/` in `.env`. In order to run your localhost on https, you will need to create a self-signed certificate and add it to the frontend root folder. You can use the following instructions to do this. Note that self-signed certificates should be used for testing purposes only, never for actual deployments. 282 | 283 | In your terminal, change to the frontend folder: 284 | 285 | ```bash 286 | cd frontend 287 | ``` 288 | 289 | Use homebrew to install mkcert: 290 | 291 | ```bash 292 | brew install mkcert 293 | ``` 294 | 295 | Then create your certificate for localhost: 296 | 297 | ```bash 298 | mkcert -install 299 | mkcert localhost 300 | ``` 301 | 302 | This will create a certificate file localhost.pem and a key file localhost-key.pem inside your client folder. 303 | 304 | Then in the package.json file in the frontend folder, replace this line on line 28 305 | 306 | ```bash 307 | "start": "react-scripts start", 308 | ``` 309 | 310 | with this line instead: 311 | 312 | ```bash 313 | "start": "HTTPS=true SSL_CRT_FILE=localhost.pem SSL_KEY_FILE=localhost-key.pem react-scripts start", 314 | ``` 315 | 316 | After starting up the Quickstart, you can now view it at https://localhost:3000. If you are on Windows, you 317 | may still get an invalid certificate warning on your browser. If so, click on "advanced" and proceed. Also on Windows, the frontend may still try to load http://localhost:3000 and you may have to access https://localhost:3000 manually. 318 | 319 | [quickstart]: https://plaid.com/docs/quickstart 320 | [libraries]: https://plaid.com/docs/api/libraries 321 | [payment-initiation]: https://plaid.com/docs/payment-initiation/ 322 | [node-example]: /node 323 | [ruby-example]: /ruby 324 | [python-example]: /python 325 | [java-example]: /java 326 | [go-example]: /go 327 | [docker]: https://www.docker.com 328 | [dashboard-api-section]: https://dashboard.plaid.com/developers/api 329 | [contact-sales]: https://plaid.com/contact 330 | -------------------------------------------------------------------------------- /assets/quickstart.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plaid/quickstart/ec8cfa1702506f6e98d5c2973be8b23c4fc72964/assets/quickstart.jpeg -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | x-environment: 3 | &QUICKSTART_ENVIRONMENT # These are read from .env file. The values in the .env file maybe overriden by shell envvars 4 | PLAID_CLIENT_ID: ${PLAID_CLIENT_ID} 5 | PLAID_SECRET: ${PLAID_SECRET} 6 | PLAID_PRODUCTS: ${PLAID_PRODUCTS} 7 | PLAID_COUNTRY_CODES: ${PLAID_COUNTRY_CODES} 8 | PLAID_REDIRECT_URI: ${PLAID_REDIRECT_URI} 9 | PLAID_ENV: ${PLAID_ENV} 10 | services: 11 | go: 12 | networks: 13 | - "quickstart" 14 | depends_on: 15 | - "frontend" 16 | build: 17 | context: . 18 | dockerfile: ./go/Dockerfile 19 | ports: ["8000:8000"] 20 | environment: 21 | <<: *QUICKSTART_ENVIRONMENT 22 | java: 23 | networks: 24 | - "quickstart" 25 | depends_on: 26 | - "frontend" 27 | build: 28 | context: . 29 | dockerfile: ./java/Dockerfile 30 | ports: ["8000:8000"] 31 | environment: 32 | <<: *QUICKSTART_ENVIRONMENT 33 | node: 34 | networks: 35 | - "quickstart" 36 | depends_on: 37 | - "frontend" 38 | build: 39 | context: . 40 | dockerfile: ./node/Dockerfile 41 | ports: ["8000:8000"] 42 | environment: 43 | <<: *QUICKSTART_ENVIRONMENT 44 | python: 45 | networks: 46 | - "quickstart" 47 | depends_on: 48 | - "frontend" 49 | build: 50 | context: . 51 | dockerfile: ./python/Dockerfile 52 | ports: ["8000:8000"] 53 | environment: 54 | <<: *QUICKSTART_ENVIRONMENT 55 | ruby: 56 | networks: 57 | - "quickstart" 58 | depends_on: 59 | - "frontend" 60 | build: 61 | context: . 62 | dockerfile: ./ruby/Dockerfile 63 | ports: ["8000:8000"] 64 | environment: 65 | <<: *QUICKSTART_ENVIRONMENT 66 | frontend: 67 | environment: 68 | - REACT_APP_API_HOST 69 | networks: 70 | - "quickstart" 71 | build: 72 | context: . 73 | dockerfile: ./frontend/Dockerfile 74 | ports: ["3000:3000"] 75 | networks: 76 | quickstart: 77 | name: quickstart 78 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .eslintcache 25 | -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.com 2 | engine-strict=true 3 | -------------------------------------------------------------------------------- /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.1 2 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM node:16-alpine 3 | 4 | # set working directory 5 | WORKDIR /app 6 | 7 | # add `/app/node_modules/.bin` to $PATH 8 | ENV PATH /app/node_modules/.bin:$PATH 9 | 10 | # install app dependencies 11 | COPY ./frontend/.npmrc ./ 12 | COPY ./frontend/package*.json ./ 13 | RUN npm install 14 | 15 | # add app 16 | COPY ./frontend ./ 17 | 18 | # start app 19 | CMD ["npm", "start"] 20 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plaid_react_quickstart", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "@testing-library/jest-dom": "^5.11.4", 6 | "@testing-library/react": "^11.1.0", 7 | "@testing-library/user-event": "^12.1.10", 8 | "@types/jest": "^26.0.15", 9 | "@types/node": "^12.0.0", 10 | "@types/react": "^16.9.53", 11 | "@types/react-dom": "^16.9.8", 12 | "ajv": "^8.11.2", 13 | "ajv-keywords": "^5.1.0", 14 | "classnames": "^2.2.6", 15 | "immer": "^9.0.6", 16 | "plaid": "^26.0.0", 17 | "plaid-threads": "^11.2.3", 18 | "react": "^16.14.0", 19 | "react-dev-utils": "^12.0.0", 20 | "react-dom": "^16.14.0", 21 | "react-plaid-link": "^3.2.1", 22 | "sass": "^1.32.8", 23 | "sass-loader": "^10.1.1", 24 | "typescript": "^4.0.3", 25 | "web-vitals": "^0.2.4" 26 | }, 27 | "devDependencies": { 28 | "@types/classnames": "^2.2.11", 29 | "react-scripts": "^5.0.1" 30 | }, 31 | "peerDependencies": { 32 | "chalk": "^4.1.2" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject", 39 | "preinstall": "npx npm-force-resolutions" 40 | }, 41 | "eslintConfig": { 42 | "extends": [ 43 | "react-app", 44 | "react-app/jest" 45 | ] 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "overrides": { 60 | "svgo": { 61 | "nth-check": ">=2.0.2" 62 | }, 63 | "react-scripts": { 64 | "postcss": ">=8.4.31" 65 | } 66 | }, 67 | "engines": { 68 | "node": ">=14.0.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Plaid Quickstart 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/App.module.scss: -------------------------------------------------------------------------------- 1 | $threads-font-path: "~plaid-threads/fonts"; 2 | @import "~plaid-threads/scss/typography"; 3 | @import "~plaid-threads/scss/variables"; 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | .container { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | min-width: 70 * $unit; 14 | max-width: 120 * $unit; 15 | margin: 0 auto; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext, useCallback } from "react"; 2 | 3 | import Header from "./Components/Headers"; 4 | import Products from "./Components/ProductTypes/Products"; 5 | import Items from "./Components/ProductTypes/Items"; 6 | import Context from "./Context"; 7 | 8 | import styles from "./App.module.scss"; 9 | import { CraCheckReportProduct } from "plaid"; 10 | 11 | const App = () => { 12 | const { linkSuccess, isPaymentInitiation, itemId, dispatch } = 13 | useContext(Context); 14 | 15 | const getInfo = useCallback(async () => { 16 | const response = await fetch("/api/info", { method: "POST" }); 17 | if (!response.ok) { 18 | dispatch({ type: "SET_STATE", state: { backend: false } }); 19 | return { paymentInitiation: false }; 20 | } 21 | const data = await response.json(); 22 | const paymentInitiation: boolean = 23 | data.products.includes("payment_initiation"); 24 | const craEnumValues = Object.values(CraCheckReportProduct); 25 | const isUserTokenFlow: boolean = data.products.some( 26 | (product: CraCheckReportProduct) => craEnumValues.includes(product) 27 | ); 28 | const isCraProductsExclusively: boolean = data.products.every( 29 | (product: CraCheckReportProduct) => craEnumValues.includes(product) 30 | ); 31 | dispatch({ 32 | type: "SET_STATE", 33 | state: { 34 | products: data.products, 35 | isPaymentInitiation: paymentInitiation, 36 | isCraProductsExclusively: isCraProductsExclusively, 37 | isUserTokenFlow: isUserTokenFlow, 38 | }, 39 | }); 40 | return { paymentInitiation, isUserTokenFlow }; 41 | }, [dispatch]); 42 | 43 | const generateUserToken = useCallback(async () => { 44 | const response = await fetch("api/create_user_token", { method: "POST" }); 45 | if (!response.ok) { 46 | dispatch({ type: "SET_STATE", state: { userToken: null } }); 47 | return; 48 | } 49 | const data = await response.json(); 50 | if (data) { 51 | if (data.error != null) { 52 | dispatch({ 53 | type: "SET_STATE", 54 | state: { 55 | linkToken: null, 56 | linkTokenError: data.error, 57 | }, 58 | }); 59 | return; 60 | } 61 | dispatch({ type: "SET_STATE", state: { userToken: data.user_token } }); 62 | return data.user_token; 63 | } 64 | }, [dispatch]); 65 | 66 | const generateToken = useCallback( 67 | async (isPaymentInitiation) => { 68 | // Link tokens for 'payment_initiation' use a different creation flow in your backend. 69 | const path = isPaymentInitiation 70 | ? "/api/create_link_token_for_payment" 71 | : "/api/create_link_token"; 72 | const response = await fetch(path, { 73 | method: "POST", 74 | }); 75 | if (!response.ok) { 76 | dispatch({ type: "SET_STATE", state: { linkToken: null } }); 77 | return; 78 | } 79 | const data = await response.json(); 80 | if (data) { 81 | if (data.error != null) { 82 | dispatch({ 83 | type: "SET_STATE", 84 | state: { 85 | linkToken: null, 86 | linkTokenError: data.error, 87 | }, 88 | }); 89 | return; 90 | } 91 | dispatch({ type: "SET_STATE", state: { linkToken: data.link_token } }); 92 | } 93 | // Save the link_token to be used later in the Oauth flow. 94 | localStorage.setItem("link_token", data.link_token); 95 | }, 96 | [dispatch] 97 | ); 98 | 99 | useEffect(() => { 100 | const init = async () => { 101 | const { paymentInitiation, isUserTokenFlow } = await getInfo(); // used to determine which path to take when generating token 102 | // do not generate a new token for OAuth redirect; instead 103 | // setLinkToken from localStorage 104 | if (window.location.href.includes("?oauth_state_id=")) { 105 | dispatch({ 106 | type: "SET_STATE", 107 | state: { 108 | linkToken: localStorage.getItem("link_token"), 109 | }, 110 | }); 111 | return; 112 | } 113 | 114 | if (isUserTokenFlow) { 115 | await generateUserToken(); 116 | } 117 | generateToken(paymentInitiation); 118 | }; 119 | init(); 120 | }, [dispatch, generateToken, generateUserToken, getInfo]); 121 | 122 | return ( 123 |
124 |
125 |
126 | {linkSuccess && ( 127 | <> 128 | 129 | {!isPaymentInitiation && itemId && } 130 | 131 | )} 132 |
133 |
134 | ); 135 | }; 136 | 137 | export default App; 138 | -------------------------------------------------------------------------------- /frontend/src/Components/Endpoint/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "~plaid-threads/scss/variables"; 2 | 3 | .endpointContainer { 4 | display: grid; 5 | grid-template-columns: 15% 57% 28%; 6 | width: 100%; 7 | border-top: 1px solid $black200; 8 | } 9 | 10 | .post { 11 | margin: 2 * $unit 0px 2 * $unit 3 * $unit; 12 | font-size: 1.4rem; 13 | } 14 | 15 | .endpointContents { 16 | margin: 3 * $unit 4 * $unit; 17 | } 18 | 19 | .endpointHeader { 20 | margin-bottom: $unit; 21 | } 22 | 23 | .endpointName { 24 | font-weight: bold; 25 | font-size: 2 * $unit; 26 | padding-right: $unit; 27 | } 28 | 29 | .schema { 30 | font-size: 2 * $unit; 31 | font-family: $font-stack-monospace; 32 | letter-spacing: -0.24px; 33 | } 34 | 35 | .endpointDescription { 36 | font-size: 1.4rem; 37 | line-height: 2 * $unit; 38 | } 39 | 40 | .buttonsContainer { 41 | display: flex; 42 | flex-direction: column; 43 | } 44 | 45 | .sendRequest { 46 | margin: 2 * $unit 2 * $unit $unit 3 * $unit; 47 | width: 70%; 48 | } 49 | 50 | .pdf { 51 | margin: 0 2 * $unit 0 3 * $unit; 52 | width: 70%; 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/Components/Endpoint/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Button from "plaid-threads/Button"; 3 | import Note from "plaid-threads/Note"; 4 | 5 | import Table from "../Table"; 6 | import Error from "../Error"; 7 | import { DataItem, Categories, ErrorDataItem, Data } from "../../dataUtilities"; 8 | 9 | import styles from "./index.module.scss"; 10 | 11 | interface Props { 12 | endpoint: string; 13 | name?: string; 14 | categories: Array; 15 | schema: string; 16 | description: string; 17 | transformData: (arg: any) => Array; 18 | } 19 | 20 | const Endpoint = (props: Props) => { 21 | const [showTable, setShowTable] = useState(false); 22 | const [transformedData, setTransformedData] = useState([]); 23 | const [pdf, setPdf] = useState(null); 24 | const [error, setError] = useState(null); 25 | const [isLoading, setIsLoading] = useState(false); 26 | 27 | const getData = async () => { 28 | setIsLoading(true); 29 | const response = await fetch(`/api/${props.endpoint}`, { method: "GET" }); 30 | const data = await response.json(); 31 | if (data.error != null) { 32 | setError(data.error); 33 | setIsLoading(false); 34 | return; 35 | } 36 | setTransformedData(props.transformData(data)); // transform data into proper format for each individual product 37 | if (data.pdf != null) { 38 | setPdf(data.pdf); 39 | } 40 | setShowTable(true); 41 | setIsLoading(false); 42 | }; 43 | 44 | const getPdfName = () => { 45 | switch(props.name) { 46 | case 'Assets': 47 | return "Asset Report.pdf"; 48 | case "CRA Base Report": 49 | return "Plaid Check Report.pdf"; 50 | case "CRA Income Insights": 51 | return "Plaid Check Report with Insights.pdf"; 52 | default: 53 | return "Statement.pdf"; 54 | } 55 | }; 56 | 57 | return ( 58 | <> 59 |
60 | 61 | POST 62 | 63 |
64 |
65 | {props.name != null && ( 66 | {props.name} 67 | )} 68 | {props.schema} 69 |
70 |
{props.description}
71 |
72 |
73 | 83 | {pdf != null && ( 84 | 94 | )} 95 |
96 |
97 | {showTable && ( 98 | 103 | )} 104 | {error != null && } 105 | 106 | ); 107 | }; 108 | 109 | Endpoint.displayName = "Endpoint"; 110 | 111 | export default Endpoint; 112 | -------------------------------------------------------------------------------- /frontend/src/Components/Error/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "~plaid-threads/scss/variables"; 2 | 3 | .errorTop { 4 | width: 90%; 5 | height: 1px; 6 | border-top: 1px solid $black200; 7 | } 8 | 9 | .errorContainer { 10 | display: grid; 11 | grid-template-columns: 15% 57% 28%; 12 | width: 100%; 13 | margin: 0; 14 | font-size: 1.4rem; 15 | } 16 | 17 | .code { 18 | margin: 2 * $unit 0px 2 * $unit 3 * $unit; 19 | font: $font-stack-monospace; 20 | font-size: 1.4rem; 21 | } 22 | 23 | .errorContents { 24 | margin: 3 * $unit 4 * $unit; 25 | 26 | } 27 | 28 | .errorItem { 29 | display: grid; 30 | grid-template-columns: 2fr 5fr; 31 | margin-bottom: $unit; 32 | } 33 | 34 | .errorTitle { 35 | font-weight: bold; 36 | line-height: normal; 37 | } 38 | 39 | .errorData { 40 | line-height: normal; 41 | font-family: $font-stack-monospace; 42 | letter-spacing: 0.25px; 43 | } 44 | 45 | .errorCode { 46 | display: inline-block; 47 | position: relative; 48 | } 49 | 50 | .pinkBox { 51 | position: absolute; 52 | top: 50%; 53 | height: 6px; 54 | width: 100%; 55 | background-color: $red200; 56 | z-index: -1; 57 | } 58 | 59 | .errorMessage { 60 | line-height: normal; 61 | } 62 | 63 | .learnMore { 64 | margin: 2 * $unit; 65 | margin-left: 3 * $unit; 66 | width: 70%; 67 | } 68 | -------------------------------------------------------------------------------- /frontend/src/Components/Error/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Button from "plaid-threads/Button"; 3 | import Note from "plaid-threads/Note"; 4 | 5 | import { ErrorDataItem } from "../../dataUtilities"; 6 | 7 | import styles from "./index.module.scss"; 8 | 9 | interface Props { 10 | error: ErrorDataItem; 11 | } 12 | 13 | const errorPaths: { [key: string]: string } = { 14 | ITEM_ERROR: "item", 15 | INSTITUTION_ERROR: "institution", 16 | API_ERROR: "api", 17 | ASSET_REPORT_ERROR: "assets", 18 | BANK_TRANSFER_ERROR: "bank-transfers", 19 | INVALID_INPUT: "invalid-input", 20 | INVALID_REQUEST: "invalid-request", 21 | INVALID_RESULT: "invalid-result", 22 | OAUTH_ERROR: "oauth", 23 | PAYMENT_ERROR: "payment", 24 | RATE_LIMIT_EXCEEDED: "rate-limit-exceeded", 25 | RECAPTCHA_ERROR: "recaptcha", 26 | SANDBOX_ERROR: "sandbox", 27 | }; 28 | 29 | const Error = (props: Props) => { 30 | const [path, setPath] = useState(""); 31 | 32 | useEffect(() => { 33 | const errorType = props.error.error_type!; 34 | const errorPath = errorPaths[errorType]; 35 | 36 | setPath( 37 | `https://plaid.com/docs/errors/${errorPath}/#${props.error.error_code?.toLowerCase()}` 38 | ); 39 | }, [props.error]); 40 | 41 | return ( 42 | <> 43 |
44 |
45 | 46 | {props.error.status_code ? props.error.status_code : "error"} 47 | 48 |
49 |
50 | Error code: 51 | 52 |
53 | {props.error.error_code} 54 |
55 |
56 |
57 |
58 |
59 | Type: 60 | {props.error.error_type} 61 |
62 |
63 | Message: 64 | 65 | {props.error.display_message == null 66 | ? props.error.error_message 67 | : props.error.display_message} 68 | 69 |
70 |
71 | 80 |
81 | 82 | ); 83 | }; 84 | 85 | Error.displayName = "Error"; 86 | 87 | export default Error; 88 | -------------------------------------------------------------------------------- /frontend/src/Components/Headers/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "~plaid-threads/scss/variables"; 2 | 3 | .grid { 4 | width: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | margin: auto; 8 | margin-bottom: 2 * $unit; 9 | } 10 | 11 | .title { 12 | margin-top: 9 * $unit; 13 | margin-bottom: 0; 14 | font-weight: 800; 15 | height: 6 * $unit; 16 | } 17 | 18 | .subtitle { 19 | margin-top: 0; 20 | margin-bottom: 3 * $unit; 21 | } 22 | 23 | .introPar { 24 | width: 100%; 25 | font-size: 2 * $unit; 26 | margin: 2 * $unit 0; 27 | } 28 | 29 | .linkButton { 30 | margin-top: 3 * $unit; 31 | } 32 | 33 | .itemAccessContainer { 34 | display: flex; 35 | flex-direction: column; 36 | height: auto; 37 | width: 100%; 38 | box-shadow: $shadow-small; 39 | margin-top: 3 * $unit; 40 | border-radius: 2px; 41 | } 42 | 43 | .itemAccessRow { 44 | display: flex; 45 | border: 1px solid $black200; 46 | height: 50%; 47 | margin: 0; 48 | 49 | &:last-child { 50 | border-top: 0px; 51 | } 52 | } 53 | 54 | .idName { 55 | padding: 2 * $unit 3 * $unit 2 * $unit 5 * $unit; 56 | flex: 1; 57 | font-weight: bold; 58 | font-family: $font-stack-monospace; 59 | color: $black1000; 60 | } 61 | 62 | .tokenText { 63 | padding: 2 * $unit 3 * $unit 2 * $unit 0; 64 | flex: 5; 65 | font-family: $font-stack-monospace; 66 | } 67 | 68 | .requests { 69 | margin-top: 7 * $unit; 70 | font-size: 2 * $unit; 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/Components/Headers/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Callout from "plaid-threads/Callout"; 3 | import Button from "plaid-threads/Button"; 4 | import InlineLink from "plaid-threads/InlineLink"; 5 | 6 | import Link from "../Link"; 7 | import Context from "../../Context"; 8 | 9 | import styles from "./index.module.scss"; 10 | 11 | const Header = () => { 12 | const { 13 | itemId, 14 | accessToken, 15 | userToken, 16 | linkToken, 17 | linkSuccess, 18 | isItemAccess, 19 | backend, 20 | linkTokenError, 21 | isPaymentInitiation, 22 | } = useContext(Context); 23 | 24 | return ( 25 |
26 |

Plaid Quickstart

27 | 28 | {!linkSuccess ? ( 29 | <> 30 |

31 | A sample end-to-end integration with Plaid 32 |

33 |

34 | The Plaid flow begins when your user wants to connect their bank 35 | account to your app. Simulate this by clicking the button below to 36 | launch Link - the client-side component that your users will 37 | interact with in order to link their accounts to Plaid and allow you 38 | to access their accounts via the Plaid API. 39 |

40 | {/* message if backend is not running and there is no link token */} 41 | {!backend ? ( 42 | 43 | Unable to fetch link_token: please make sure your backend server 44 | is running and that your .env file has been configured with your 45 | PLAID_CLIENT_ID and PLAID_SECRET. 46 | 47 | ) : /* message if backend is running and there is no link token */ 48 | linkToken == null && backend ? ( 49 | 50 |
51 | Unable to fetch link_token: please make sure your backend server 52 | is running and that your .env file has been configured 53 | correctly. 54 |
55 |
56 | If you are on a Windows machine, please ensure that you have 57 | cloned the repo with{" "} 58 | 62 | symlinks turned on. 63 | {" "} 64 | You can also try checking your{" "} 65 | 69 | activity log 70 | {" "} 71 | on your Plaid dashboard. 72 |
73 |
74 | Error Code: {linkTokenError.error_code} 75 |
76 |
77 | Error Type: {linkTokenError.error_type}{" "} 78 |
79 |
Error Message: {linkTokenError.error_message}
80 |
81 | ) : linkToken === "" ? ( 82 |
83 | 86 |
87 | ) : ( 88 |
89 | 90 |
91 | )} 92 | 93 | ) : ( 94 | <> 95 | {isPaymentInitiation ? ( 96 | <> 97 |

98 | Congrats! Your payment is now confirmed. 99 |

100 | 101 | You can see information of all your payments in the{" "} 102 | 106 | Payments Dashboard 107 | 108 | . 109 | 110 |

111 |

112 | Now that the 'payment_id' stored in your server, you can use it 113 | to access the payment information: 114 |

115 | 116 | ) : ( 117 | /* If not using the payment_initiation product, show the item_id and access_token information */ <> 118 | {isItemAccess ? ( 119 |

120 | Congrats! By linking an account, you have created an{" "} 121 | 125 | Item 126 | 127 | . 128 |

129 | ) : userToken ? ( 130 |

131 | Congrats! You have successfully linked data to a User. 132 |

133 | ) : ( 134 |

135 | 136 | Unable to create an item. Please check your backend server 137 | 138 |

139 | )} 140 |
141 | {itemId && ( 142 |

143 | item_id 144 | {itemId} 145 |

146 | )} 147 | 148 | {accessToken && ( 149 |

150 | access_token 151 | {accessToken} 152 |

153 | )} 154 | 155 | {userToken && ( 156 |

157 | user_token 158 | {userToken} 159 |

160 | )} 161 |
162 | {(isItemAccess || userToken) && ( 163 |

164 | Now that you have {accessToken && "an access_token"} 165 | {accessToken && userToken && " and "} 166 | {userToken && "a user_token"}, you can make all of the 167 | following requests: 168 |

169 | )} 170 | 171 | )} 172 | 173 | )} 174 |
175 | ); 176 | }; 177 | 178 | Header.displayName = "Header"; 179 | 180 | export default Header; 181 | -------------------------------------------------------------------------------- /frontend/src/Components/Link/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from "react"; 2 | import { usePlaidLink } from "react-plaid-link"; 3 | import Button from "plaid-threads/Button"; 4 | 5 | import Context from "../../Context"; 6 | 7 | const Link = () => { 8 | const { linkToken, isPaymentInitiation, isCraProductsExclusively, dispatch } = 9 | useContext(Context); 10 | 11 | const onSuccess = React.useCallback( 12 | (public_token: string) => { 13 | // If the access_token is needed, send public_token to server 14 | const exchangePublicTokenForAccessToken = async () => { 15 | const response = await fetch("/api/set_access_token", { 16 | method: "POST", 17 | headers: { 18 | "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", 19 | }, 20 | body: `public_token=${public_token}`, 21 | }); 22 | if (!response.ok) { 23 | dispatch({ 24 | type: "SET_STATE", 25 | state: { 26 | itemId: `no item_id retrieved`, 27 | accessToken: `no access_token retrieved`, 28 | isItemAccess: false, 29 | }, 30 | }); 31 | return; 32 | } 33 | const data = await response.json(); 34 | dispatch({ 35 | type: "SET_STATE", 36 | state: { 37 | itemId: data.item_id, 38 | accessToken: data.access_token, 39 | isItemAccess: true, 40 | }, 41 | }); 42 | }; 43 | 44 | // 'payment_initiation' products do not require the public_token to be exchanged for an access_token. 45 | if (isPaymentInitiation) { 46 | dispatch({ type: "SET_STATE", state: { isItemAccess: false } }); 47 | } else if (isCraProductsExclusively) { 48 | // When only CRA products are enabled, only user_token is needed. access_token/public_token exchange is not needed. 49 | dispatch({ type: "SET_STATE", state: { isItemAccess: false } }); 50 | } else { 51 | exchangePublicTokenForAccessToken(); 52 | } 53 | 54 | dispatch({ type: "SET_STATE", state: { linkSuccess: true } }); 55 | window.history.pushState("", "", "/"); 56 | }, 57 | [dispatch, isPaymentInitiation, isCraProductsExclusively] 58 | ); 59 | 60 | let isOauth = false; 61 | const config: Parameters[0] = { 62 | token: linkToken!, 63 | onSuccess, 64 | }; 65 | 66 | if (window.location.href.includes("?oauth_state_id=")) { 67 | // TODO: figure out how to delete this ts-ignore 68 | // @ts-ignore 69 | config.receivedRedirectUri = window.location.href; 70 | isOauth = true; 71 | } 72 | 73 | const { open, ready } = usePlaidLink(config); 74 | 75 | useEffect(() => { 76 | if (isOauth && ready) { 77 | open(); 78 | } 79 | }, [ready, open, isOauth]); 80 | 81 | return ( 82 | 85 | ); 86 | }; 87 | 88 | Link.displayName = "Link"; 89 | 90 | export default Link; 91 | -------------------------------------------------------------------------------- /frontend/src/Components/ProductTypes/Items.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Endpoint from "../Endpoint"; 4 | import ProductTypesContainer from "./ProductTypesContainer"; 5 | import { 6 | transformItemData, 7 | transformAccountsData, 8 | itemCategories, 9 | accountsCategories, 10 | } from "../../dataUtilities"; 11 | 12 | const Items = () => ( 13 | <> 14 | 15 | 24 | 31 | 32 | 33 | ); 34 | 35 | Items.displayName = "Items"; 36 | 37 | export default Items; 38 | -------------------------------------------------------------------------------- /frontend/src/Components/ProductTypes/ProductTypesContainer/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "~plaid-threads/scss/variables"; 2 | 3 | .container { 4 | width: 100%; 5 | border: 1px solid $black200; 6 | border-radius: 2px; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | box-shadow: $shadow-small; 11 | margin: 0 0 3 * $unit 0; 12 | } 13 | 14 | .header { 15 | width: 100%; 16 | border-bottom: 0px; 17 | height: 13 * $unit; 18 | font-weight: 800; 19 | padding-top: 5 * $unit; 20 | padding-left: 5 * $unit; 21 | margin-top: 0; 22 | margin-bottom: 0; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/Components/ProductTypes/ProductTypesContainer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import styles from "./index.module.scss"; 4 | 5 | interface Props { 6 | children?: React.ReactNode | Array; 7 | productType: string; 8 | } 9 | 10 | const TypeContainer: React.FC = (props) => ( 11 |
12 |

{props.productType}

13 | {props.children} 14 |
15 | ); 16 | 17 | TypeContainer.displayName = "TypeContainer"; 18 | 19 | export default TypeContainer; 20 | -------------------------------------------------------------------------------- /frontend/src/Components/ProductTypes/Products.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | import Endpoint from "../Endpoint"; 4 | import Context from "../../Context"; 5 | import ProductTypesContainer from "./ProductTypesContainer"; 6 | import { 7 | transactionsCategories, 8 | authCategories, 9 | identityCategories, 10 | balanceCategories, 11 | investmentsCategories, 12 | investmentsTransactionsCategories, 13 | liabilitiesCategories, 14 | paymentCategories, 15 | assetsCategories, 16 | incomePaystubsCategories, 17 | transferCategories, 18 | transferAuthorizationCategories, 19 | signalCategories, 20 | statementsCategories, 21 | transformAuthData, 22 | transformTransactionsData, 23 | transformBalanceData, 24 | transformInvestmentsData, 25 | transformInvestmentTransactionsData, 26 | transformLiabilitiesData, 27 | transformIdentityData, 28 | transformPaymentData, 29 | transformAssetsData, 30 | transformTransferData, 31 | transformTransferAuthorizationData, 32 | transformIncomePaystubsData, 33 | transformSignalData, 34 | transformStatementsData, 35 | transformBaseReportGetData, 36 | transformIncomeInsightsData, 37 | checkReportBaseReportCategories, 38 | checkReportInsightsCategories, 39 | transformPartnerInsightsData, 40 | checkReportPartnerInsightsCategories 41 | } from "../../dataUtilities"; 42 | 43 | const Products = () => { 44 | const { products, isCraProductsExclusively } = useContext(Context); 45 | return ( 46 | 47 | {products.includes("payment_initiation") && ( 48 | 56 | )} 57 | {products.includes("auth") && ( 58 | 66 | )} 67 | {products.includes("transactions") && ( 68 | 76 | )} 77 | {products.includes("identity") && ( 78 | 87 | )} 88 | {products.includes("assets") && ( 89 | 97 | )} 98 | {!products.includes("payment_initiation") && !isCraProductsExclusively && ( 99 | 108 | )} 109 | {products.includes("investments") && ( 110 | <> 111 | 121 | 130 | 138 | 139 | )} 140 | {products.includes("transfer") && ( 141 | <> 142 | 150 | 158 | 159 | )} 160 | {products.includes("signal") && ( 161 | <> 162 | 170 | 171 | )} 172 | {products.includes("statements") && ( 173 | <> 174 | 182 | 183 | )} 184 | 185 | {products.includes("income_verification") && ( 186 | 194 | )} 195 | 196 | {(products.includes("cra_base_report") || products.includes("cra_income_insights")) && ( 197 | 205 | )} 206 | 207 | {(products.includes("cra_base_report") || products.includes("cra_income_insights")) && ( 208 | 216 | )} 217 | 218 | {products.includes("cra_partner_insights") && ( 219 | 227 | )} 228 | 229 | ); 230 | }; 231 | 232 | Products.displayName = "Products"; 233 | 234 | export default Products; 235 | -------------------------------------------------------------------------------- /frontend/src/Components/Table/Identity.module.scss: -------------------------------------------------------------------------------- 1 | @import "~plaid-threads/scss/variables"; 2 | 3 | .identityTable { 4 | width: 90%; 5 | margin-bottom: 3 * $unit; 6 | font-size: 1.4rem; 7 | line-height: normal; 8 | } 9 | 10 | .identityHeadersRow { 11 | display: grid; 12 | grid-template-columns: 25% 25% 25% 25%; 13 | grid-gap: $unit; 14 | border-top: 1px solid $black200; 15 | border-bottom: 1px solid $black1000; 16 | font-weight: bold; 17 | } 18 | 19 | .identityHeader { 20 | line-height: 2 * $unit; 21 | position: relative; 22 | bottom: 0; 23 | padding: $unit 0 0.4rem 0; 24 | } 25 | 26 | .identityDataBody { 27 | margin-top: 2 * $unit; 28 | } 29 | 30 | .identityDataRow { 31 | display: grid; 32 | grid-template-columns: 25% 25% 25% 25%; 33 | word-wrap: break-word; 34 | grid-gap: $unit; 35 | margin-bottom: 5px; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/Components/Table/Identity.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { DataItem, Categories } from "../../dataUtilities"; 4 | 5 | import styles from "./Identity.module.scss"; 6 | 7 | interface Props { 8 | data: Array; 9 | categories: Array; 10 | } 11 | 12 | const Identity = (props: Props) => { 13 | const identityHeaders = props.categories.map((category, index) => ( 14 | 15 | {category.title} 16 | 17 | )); 18 | 19 | const identityRows = props.data.map((item: DataItem | any, index) => ( 20 |
21 | {props.categories.map((category: Categories, index) => ( 22 | 23 | {item[category.field]} 24 | 25 | ))} 26 |
27 | )); 28 | 29 | return ( 30 |
31 |
{identityHeaders}
32 |
{identityRows}
33 |
34 | ); 35 | }; 36 | 37 | Identity.displayName = "Identity"; 38 | 39 | export default Identity; 40 | -------------------------------------------------------------------------------- /frontend/src/Components/Table/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "~plaid-threads/scss/variables"; 2 | 3 | .dataTable { 4 | width: 90%; 5 | margin-bottom: 3 * $unit; 6 | border-spacing: 0px; 7 | font-size: 1.4rem; 8 | } 9 | 10 | .headerRow { 11 | text-align: left; 12 | height: 4 * $unit; 13 | } 14 | 15 | .headerField { 16 | border-top: 1px solid $black200; 17 | border-bottom: 1px solid $black1000; 18 | padding-top: 1.2rem; 19 | padding-bottom: $unit; 20 | padding-left: $unit; 21 | } 22 | 23 | .dataRows { 24 | height: 6 * $unit; 25 | &:first-child { 26 | td { 27 | border-top: 0px; 28 | } 29 | } 30 | &:last-child { 31 | margin-bottom: 2 * $unit; 32 | } 33 | } 34 | 35 | .dataField { 36 | text-align: left; 37 | overflow-wrap: break-word; 38 | border-top: 1px solid $black200; 39 | max-width: 25 * $unit; 40 | vertical-align: top; 41 | padding-top: 2 * $unit; 42 | padding-left: $unit; 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/Components/Table/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { DataItem, Categories } from "../../dataUtilities"; 4 | import Identity from "./Identity"; 5 | 6 | import styles from "./index.module.scss"; 7 | 8 | interface Props { 9 | data: Array; 10 | categories: Array; 11 | isIdentity: boolean; 12 | } 13 | 14 | const Table = (props: Props) => { 15 | const maxRows = 15; 16 | // regular table 17 | const headers = props.categories.map((category, index) => ( 18 |
21 | )); 22 | 23 | const rows = props.data 24 | .map((item: DataItem | any, index) => ( 25 | 26 | {props.categories.map((category: Categories, index) => ( 27 | 30 | ))} 31 | 32 | )) 33 | .slice(0, maxRows); 34 | 35 | return props.isIdentity ? ( 36 | 37 | ) : ( 38 |
19 | {category.title} 20 |
28 | {item[category.field]} 29 |
39 | 40 | {headers} 41 | 42 | {rows} 43 |
44 | ); 45 | }; 46 | 47 | Table.displayName = "Table"; 48 | 49 | export default Table; 50 | -------------------------------------------------------------------------------- /frontend/src/Context/index.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useReducer, Dispatch, ReactNode } from "react"; 2 | 3 | interface QuickstartState { 4 | linkSuccess: boolean; 5 | isItemAccess: boolean; 6 | isPaymentInitiation: boolean; 7 | isUserTokenFlow: boolean; 8 | isCraProductsExclusively: boolean; 9 | linkToken: string | null; 10 | accessToken: string | null; 11 | userToken: string | null; 12 | itemId: string | null; 13 | isError: boolean; 14 | backend: boolean; 15 | products: string[]; 16 | linkTokenError: { 17 | error_message: string; 18 | error_code: string; 19 | error_type: string; 20 | }; 21 | } 22 | 23 | const initialState: QuickstartState = { 24 | linkSuccess: false, 25 | isItemAccess: true, 26 | isPaymentInitiation: false, 27 | isCraProductsExclusively: false, 28 | isUserTokenFlow: false, 29 | linkToken: "", // Don't set to null or error message will show up briefly when site loads 30 | userToken: null, 31 | accessToken: null, 32 | itemId: null, 33 | isError: false, 34 | backend: true, 35 | products: ["transactions"], 36 | linkTokenError: { 37 | error_type: "", 38 | error_code: "", 39 | error_message: "", 40 | }, 41 | }; 42 | 43 | type QuickstartAction = { 44 | type: "SET_STATE"; 45 | state: Partial; 46 | }; 47 | 48 | interface QuickstartContext extends QuickstartState { 49 | dispatch: Dispatch; 50 | } 51 | 52 | const Context = createContext( 53 | initialState as QuickstartContext 54 | ); 55 | 56 | const { Provider } = Context; 57 | export const QuickstartProvider: React.FC<{ children: ReactNode }> = ( 58 | props 59 | ) => { 60 | const reducer = ( 61 | state: QuickstartState, 62 | action: QuickstartAction 63 | ): QuickstartState => { 64 | switch (action.type) { 65 | case "SET_STATE": 66 | return { ...state, ...action.state }; 67 | default: 68 | return { ...state }; 69 | } 70 | }; 71 | const [state, dispatch] = useReducer(reducer, initialState); 72 | return {props.children}; 73 | }; 74 | 75 | export default Context; 76 | -------------------------------------------------------------------------------- /frontend/src/dataUtilities.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountsGetResponse, 3 | AssetReport, 4 | AuthGetResponse, 5 | CraCheckReportBaseReportGetResponse, 6 | CraCheckReportIncomeInsightsGetResponse, 7 | CraCheckReportPartnerInsightsGetResponse, 8 | IdentityGetResponse, 9 | IncomeVerificationPaystubsGetResponse, 10 | InstitutionsGetByIdResponse, 11 | InvestmentsHoldingsGetResponse, 12 | InvestmentsTransactionsGetResponse, 13 | ItemGetResponse, 14 | LiabilitiesGetResponse, 15 | PaymentInitiationPaymentGetResponse, 16 | Paystub, 17 | SignalEvaluateResponse, 18 | StatementsListResponse, 19 | Transaction, 20 | TransferAuthorizationCreateResponse, 21 | TransferCreateResponse, 22 | } from "plaid/dist/api"; 23 | 24 | const formatCurrency = ( 25 | number: number | null | undefined, 26 | code: string | null | undefined 27 | ) => { 28 | if (number != null && number !== undefined) { 29 | return ` ${parseFloat(number.toFixed(2)).toLocaleString("en")} ${code}`; 30 | } 31 | return "no data"; 32 | }; 33 | 34 | export interface Categories { 35 | title: string; 36 | field: string; 37 | } 38 | 39 | //interfaces for categories in each individual product 40 | interface AuthDataItem { 41 | routing: string; 42 | account: string; 43 | balance: string; 44 | name: string; 45 | } 46 | interface TransactionsDataItem { 47 | amount: string; 48 | date: string; 49 | name: string; 50 | } 51 | 52 | interface IdentityDataItem { 53 | addresses: string; 54 | phoneNumbers: string; 55 | emails: string; 56 | names: string; 57 | } 58 | 59 | interface BalanceDataItem { 60 | balance: string; 61 | subtype: string | null; 62 | mask: string; 63 | name: string; 64 | } 65 | 66 | interface InvestmentsDataItem { 67 | mask: string; 68 | quantity: string; 69 | price: string; 70 | value: string; 71 | name: string; 72 | } 73 | 74 | interface InvestmentsTransactionItem { 75 | amount: number; 76 | date: string; 77 | name: string; 78 | } 79 | 80 | interface LiabilitiessDataItem { 81 | amount: string; 82 | date: string; 83 | name: string; 84 | type: string; 85 | } 86 | 87 | interface PaymentDataItem { 88 | paymentId: string; 89 | amount: string; 90 | status: string; 91 | statusUpdate: string; 92 | recipientId: string; 93 | } 94 | interface ItemDataItem { 95 | billed: string; 96 | available: string; 97 | name: string; 98 | } 99 | 100 | interface AssetsDataItem { 101 | account: string; 102 | balance: string; 103 | transactions: number; 104 | daysAvailable: number; 105 | } 106 | 107 | interface TransferDataItem { 108 | transferId: string; 109 | amount: string; 110 | type: string; 111 | achClass: string | null; 112 | network: string; 113 | } 114 | 115 | interface TransferAuthorizationDataItem { 116 | authorizationId: string; 117 | authorizationDecision: string; 118 | decisionRationaleCode: string | null; 119 | decisionRationaleDescription: string | null; 120 | } 121 | 122 | interface StatementsDataItem { 123 | account: string | null; 124 | date: string | null; 125 | } 126 | 127 | interface SignalDataItem { 128 | customerInitiatedReturnRiskScore: number | undefined | null; 129 | customerInitiatedReturnRiskTier: number | undefined | null; 130 | bankInitiatedReturnRiskScore: number | undefined | null; 131 | bankInitiatedReturnRiskTier: number | undefined | null; 132 | daysSinceFirstPlaidConnection: number | undefined | null; 133 | } 134 | 135 | interface IncomePaystubsDataItem { 136 | description: string; 137 | currentAmount: number | null; 138 | currency: number | null; 139 | } 140 | 141 | interface CreditReportGetItem { 142 | institution: string; 143 | accountName: string; 144 | averageDaysBetweenTransactions: string | null; 145 | averageInflowAmount: string | null; 146 | averageOutflowAmount: string | null; 147 | averageBalance: string | null; 148 | balance: string | null; 149 | } 150 | 151 | interface CreditInsightsGetItem { 152 | incomeSourcesCount: number | null; 153 | historicalAnnualIncome: string | null; 154 | forecastedAnnualIncome: string | null; 155 | } 156 | 157 | interface CreditPartnerInsightsGetItem { 158 | firstDetectScore: number | null; 159 | cashScore: number | null; 160 | } 161 | 162 | export interface ErrorDataItem { 163 | error_type: string; 164 | error_code: string; 165 | error_message: string; 166 | display_message: string | null; 167 | status_code: number | null; 168 | } 169 | 170 | //all possible product data interfaces 171 | export type DataItem = 172 | | AuthDataItem 173 | | TransactionsDataItem 174 | | IdentityDataItem 175 | | BalanceDataItem 176 | | InvestmentsDataItem 177 | | InvestmentsTransactionItem 178 | | LiabilitiessDataItem 179 | | ItemDataItem 180 | | PaymentDataItem 181 | | AssetsDataItem 182 | | TransferDataItem 183 | | TransferAuthorizationDataItem 184 | | IncomePaystubsDataItem 185 | | SignalDataItem 186 | | StatementsDataItem 187 | | CreditReportGetItem 188 | | CreditInsightsGetItem 189 | | CreditPartnerInsightsGetItem; 190 | 191 | export type Data = Array; 192 | 193 | export const authCategories: Array = [ 194 | { 195 | title: "Name", 196 | field: "name", 197 | }, 198 | { 199 | title: "Balance", 200 | field: "balance", 201 | }, 202 | { 203 | title: "Account #", 204 | field: "account", 205 | }, 206 | { 207 | title: "Routing #", 208 | field: "routing", 209 | }, 210 | ]; 211 | 212 | export const transactionsCategories: Array = [ 213 | { 214 | title: "Name", 215 | field: "name", 216 | }, 217 | { 218 | title: "Amount", 219 | field: "amount", 220 | }, 221 | { 222 | title: "Date", 223 | field: "date", 224 | }, 225 | ]; 226 | 227 | export const identityCategories: Array = [ 228 | { 229 | title: "Names", 230 | field: "names", 231 | }, 232 | { 233 | title: "Emails", 234 | field: "emails", 235 | }, 236 | { 237 | title: "Phone numbers", 238 | field: "phoneNumbers", 239 | }, 240 | { 241 | title: "Addresses", 242 | field: "addresses", 243 | }, 244 | ]; 245 | 246 | export const balanceCategories: Array = [ 247 | { 248 | title: "Name", 249 | field: "name", 250 | }, 251 | { 252 | title: "Balance", 253 | field: "balance", 254 | }, 255 | { 256 | title: "Subtype", 257 | field: "subtype", 258 | }, 259 | { 260 | title: "Mask", 261 | field: "mask", 262 | }, 263 | ]; 264 | 265 | export const investmentsCategories: Array = [ 266 | { 267 | title: "Account Mask", 268 | field: "mask", 269 | }, 270 | { 271 | title: "Name", 272 | field: "name", 273 | }, 274 | { 275 | title: "Quantity", 276 | field: "quantity", 277 | }, 278 | { 279 | title: "Close Price", 280 | field: "price", 281 | }, 282 | { 283 | title: "Value", 284 | field: "value", 285 | }, 286 | ]; 287 | 288 | export const investmentsTransactionsCategories: Array = [ 289 | { 290 | title: "Name", 291 | field: "name", 292 | }, 293 | { 294 | title: "Amount", 295 | field: "amount", 296 | }, 297 | { 298 | title: "Date", 299 | field: "date", 300 | }, 301 | ]; 302 | 303 | export const liabilitiesCategories: Array = [ 304 | { 305 | title: "Name", 306 | field: "name", 307 | }, 308 | { 309 | title: "Type", 310 | field: "type", 311 | }, 312 | { 313 | title: "Last Payment Date", 314 | field: "date", 315 | }, 316 | { 317 | title: "Last Payment Amount", 318 | field: "amount", 319 | }, 320 | ]; 321 | 322 | export const itemCategories: Array = [ 323 | { 324 | title: "Institution Name", 325 | field: "name", 326 | }, 327 | { 328 | title: "Billed Products", 329 | field: "billed", 330 | }, 331 | { 332 | title: "Available Products", 333 | field: "available", 334 | }, 335 | ]; 336 | 337 | export const accountsCategories: Array = [ 338 | { 339 | title: "Name", 340 | field: "name", 341 | }, 342 | { 343 | title: "Balance", 344 | field: "balance", 345 | }, 346 | { 347 | title: "Subtype", 348 | field: "subtype", 349 | }, 350 | { 351 | title: "Mask", 352 | field: "mask", 353 | }, 354 | ]; 355 | 356 | export const paymentCategories: Array = [ 357 | { 358 | title: "Payment ID", 359 | field: "paymentId", 360 | }, 361 | { 362 | title: "Amount", 363 | field: "amount", 364 | }, 365 | { 366 | title: "Status", 367 | field: "status", 368 | }, 369 | { 370 | title: "Status Update", 371 | field: "statusUpdate", 372 | }, 373 | { 374 | title: "Recipient ID", 375 | field: "recipientId", 376 | }, 377 | ]; 378 | 379 | export const assetsCategories: Array = [ 380 | { 381 | title: "Account", 382 | field: "account", 383 | }, 384 | { 385 | title: "Transactions", 386 | field: "transactions", 387 | }, 388 | { 389 | title: "Balance", 390 | field: "balance", 391 | }, 392 | { 393 | title: "Days Available", 394 | field: "daysAvailable", 395 | }, 396 | ]; 397 | 398 | export const transferCategories: Array = [ 399 | { 400 | title: "Transfer ID", 401 | field: "transferId", 402 | }, 403 | { 404 | title: "Amount", 405 | field: "amount", 406 | }, 407 | { 408 | title: "Type", 409 | field: "type", 410 | }, 411 | { 412 | title: "ACH Class", 413 | field: "achClass", 414 | }, 415 | { 416 | title: "Network", 417 | field: "network", 418 | }, 419 | { 420 | title: "Status", 421 | field: "status", 422 | }, 423 | ]; 424 | 425 | export const transferAuthorizationCategories: Array = [ 426 | { 427 | title: "Authorization ID", 428 | field: "authorizationId", 429 | }, 430 | { 431 | title: "Authorization Decision", 432 | field: "authorizationDecision", 433 | }, 434 | { 435 | title: "Decision rationale code", 436 | field: "decisionRationaleCode", 437 | }, 438 | { 439 | title: "Decision rationale description", 440 | field: "decisionRationaleDescription", 441 | }, 442 | ]; 443 | 444 | export const signalCategories: Array = [ 445 | { 446 | title: "Customer-initiated return risk score", 447 | field: "customerInitiatedReturnRiskScore", 448 | }, 449 | 450 | { 451 | title: "Customer-initiated return risk tier", 452 | field: "customerInitiatedReturnRiskTier", 453 | }, 454 | { 455 | title: "Bank-initiated return risk score", 456 | field: "bankInitiatedReturnRiskScore", 457 | }, 458 | { 459 | title: "Bank-initiated return risk tier", 460 | field: "bankInitiatedReturnRiskTier", 461 | }, 462 | { 463 | title: "Sample core attribute: Days since first Plaid connection", 464 | field: "daysSinceFirstPlaidConnection", 465 | }, 466 | ]; 467 | 468 | export const statementsCategories: Array = [ 469 | { 470 | title: "Account name", 471 | field: "account" 472 | }, 473 | { 474 | title: "Statement Date", 475 | field: "date" 476 | } 477 | ]; 478 | 479 | export const incomePaystubsCategories: Array = [ 480 | { 481 | title: "Description", 482 | field: "description", 483 | }, 484 | { 485 | title: "Current Amount", 486 | field: "currentAmount", 487 | }, 488 | { 489 | title: "Currency", 490 | field: "currency", 491 | }, 492 | ]; 493 | 494 | 495 | export const checkReportBaseReportCategories: Array = [ 496 | { 497 | title: "Account Name", 498 | field: "accountName" 499 | }, 500 | { 501 | title: "Balance", 502 | field: "balance" 503 | }, 504 | { 505 | title: "Avg. Balance", 506 | field: "averageBalance" 507 | }, 508 | { 509 | title: "Avg. Inflow Amount", 510 | field: "averageInflowAmount" 511 | }, 512 | { 513 | title: "Avg. Outflow Amount", 514 | field: "averageOutflowAmount" 515 | }, 516 | { 517 | title: "Avg. Days Between Transactions", 518 | field: "averageDaysBetweenTransactions" 519 | } 520 | ]; 521 | 522 | export const checkReportInsightsCategories: Array = [ 523 | { 524 | title: "Income Sources", 525 | field: "incomeSourcesCount", 526 | }, 527 | { 528 | title: "Historical Annual Income", 529 | field: "historicalAnnualIncome", 530 | }, 531 | { 532 | title: "Forecasted Annual Income", 533 | field: "forecastedAnnualIncome", 534 | } 535 | ]; 536 | 537 | export const checkReportPartnerInsightsCategories: Array = [ 538 | { 539 | title: "CashScore®", 540 | field: "cashScore", 541 | }, 542 | { 543 | title: "FirstDetect Score", 544 | field: "firstDetectScore", 545 | } 546 | ]; 547 | 548 | export const transformAuthData = (data: AuthGetResponse) => { 549 | return data.numbers.ach!.map((achNumbers) => { 550 | const account = data.accounts!.filter((a) => { 551 | return a.account_id === achNumbers.account_id; 552 | })[0]; 553 | const balance: number | null | undefined = 554 | account.balances.available || account.balances.current; 555 | const obj: DataItem = { 556 | name: account.name, 557 | balance: formatCurrency(balance, account.balances.iso_currency_code), 558 | account: achNumbers.account!, 559 | routing: achNumbers.routing!, 560 | }; 561 | return obj; 562 | }); 563 | }; 564 | 565 | export const transformStatementsData = (data: {json: StatementsListResponse}) => { 566 | const account = data.json.accounts[0]!.account_name; 567 | const statements = data.json.accounts[0]!.statements; 568 | return statements!.map((s) => { 569 | const item: DataItem = { 570 | date: Intl.DateTimeFormat('en', { month: 'long', year:'numeric' }).format(new Date(s.year!, s.month!)), 571 | account: account, 572 | }; 573 | return item; 574 | }); 575 | }; 576 | 577 | export const transformTransactionsData = (data: { 578 | latest_transactions: Transaction[]; 579 | }): Array => { 580 | return data.latest_transactions!.map((t) => { 581 | const item: DataItem = { 582 | name: t.name!, 583 | amount: formatCurrency(t.amount!, t.iso_currency_code), 584 | date: t.date, 585 | }; 586 | return item; 587 | }); 588 | }; 589 | 590 | interface IdentityData { 591 | identity: IdentityGetResponse["accounts"]; 592 | } 593 | 594 | export const transformIdentityData = (data: IdentityData) => { 595 | const final: Array = []; 596 | const identityData = data.identity![0]; 597 | identityData.owners.forEach((owner) => { 598 | const names = owner.names.map((name) => { 599 | return name; 600 | }); 601 | const emails = owner.emails.map((email) => { 602 | return email.data; 603 | }); 604 | const phones = owner.phone_numbers.map((phone) => { 605 | return phone.data; 606 | }); 607 | const addresses = owner.addresses.map((address) => { 608 | return `${address.data.street} ${address.data.city}, ${address.data.region} ${address.data.postal_code}`; 609 | }); 610 | 611 | const num = Math.max( 612 | emails.length, 613 | names.length, 614 | phones.length, 615 | addresses.length 616 | ); 617 | 618 | for (let i = 0; i < num; i++) { 619 | const obj = { 620 | names: names[i] || "", 621 | emails: emails[i] || "", 622 | phoneNumbers: phones[i] || "", 623 | addresses: addresses[i] || "", 624 | }; 625 | final.push(obj); 626 | } 627 | }); 628 | 629 | return final; 630 | }; 631 | 632 | export const transformBalanceData = (data: AccountsGetResponse) => { 633 | const balanceData = data.accounts; 634 | return balanceData.map((account) => { 635 | const balance: number | null | undefined = 636 | account.balances.available || account.balances.current; 637 | const obj: DataItem = { 638 | name: account.name, 639 | balance: formatCurrency(balance, account.balances.iso_currency_code), 640 | subtype: account.subtype, 641 | mask: account.mask!, 642 | }; 643 | return obj; 644 | }); 645 | }; 646 | 647 | interface InvestmentData { 648 | error: null; 649 | holdings: InvestmentsHoldingsGetResponse; 650 | } 651 | 652 | export const transformInvestmentsData = (data: InvestmentData) => { 653 | const holdingsData = data.holdings.holdings!.sort(function (a, b) { 654 | if (a.account_id > b.account_id) return 1; 655 | return -1; 656 | }); 657 | return holdingsData.map((holding) => { 658 | const account = data.holdings.accounts!.filter( 659 | (acc) => acc.account_id === holding.account_id 660 | )[0]; 661 | const security = data.holdings.securities!.filter( 662 | (sec) => sec.security_id === holding.security_id 663 | )[0]; 664 | const value = holding.quantity * security.close_price!; 665 | 666 | const obj: DataItem = { 667 | mask: account.mask!, 668 | name: security.name!, 669 | quantity: formatCurrency(holding.quantity, ""), 670 | price: formatCurrency( 671 | security.close_price!, 672 | account.balances.iso_currency_code 673 | ), 674 | value: formatCurrency(value, account.balances.iso_currency_code), 675 | }; 676 | return obj; 677 | }); 678 | }; 679 | 680 | interface InvestmentsTransactionData { 681 | error: null; 682 | investments_transactions: InvestmentsTransactionsGetResponse; 683 | } 684 | 685 | export const transformInvestmentTransactionsData = ( 686 | data: InvestmentsTransactionData 687 | ) => { 688 | const investmentTransactionsData = 689 | data.investments_transactions.investment_transactions!.sort(function ( 690 | a, 691 | b 692 | ) { 693 | if (a.account_id > b.account_id) return 1; 694 | return -1; 695 | }); 696 | return investmentTransactionsData.map((investmentTransaction) => { 697 | const security = data.investments_transactions.securities!.filter( 698 | (sec) => sec.security_id === investmentTransaction.security_id 699 | )[0]; 700 | 701 | const obj: DataItem = { 702 | name: security.name!, 703 | amount: investmentTransaction.amount, 704 | date: investmentTransaction.date, 705 | }; 706 | return obj; 707 | }); 708 | }; 709 | 710 | interface LiabilitiesDataResponse { 711 | error: null; 712 | liabilities: LiabilitiesGetResponse; 713 | } 714 | 715 | export const transformLiabilitiesData = (data: LiabilitiesDataResponse) => { 716 | const liabilitiesData = data.liabilities.liabilities; 717 | //console.log(liabilitiesData) 718 | //console.log("random") 719 | const credit = liabilitiesData.credit!.map((credit) => { 720 | const account = data.liabilities.accounts.filter( 721 | (acc) => acc.account_id === credit.account_id 722 | )[0]; 723 | const obj: DataItem = { 724 | name: account.name, 725 | type: "credit card", 726 | date: credit.last_payment_date ?? "", 727 | amount: formatCurrency( 728 | credit.last_payment_amount, 729 | account.balances.iso_currency_code 730 | ), 731 | }; 732 | return obj; 733 | }); 734 | 735 | const mortgages = liabilitiesData.mortgage?.map((mortgage) => { 736 | const account = data.liabilities.accounts.filter( 737 | (acc) => acc.account_id === mortgage.account_id 738 | )[0]; 739 | const obj: DataItem = { 740 | name: account.name, 741 | type: "mortgage", 742 | date: mortgage.last_payment_date!, 743 | amount: formatCurrency( 744 | mortgage.last_payment_amount!, 745 | account.balances.iso_currency_code 746 | ), 747 | }; 748 | return obj; 749 | }); 750 | 751 | const student = liabilitiesData.student?.map((student) => { 752 | const account = data.liabilities.accounts.filter( 753 | (acc) => acc.account_id === student.account_id 754 | )[0]; 755 | const obj: DataItem = { 756 | name: account.name, 757 | type: "student loan", 758 | date: student.last_payment_date!, 759 | amount: formatCurrency( 760 | student.last_payment_amount!, 761 | account.balances.iso_currency_code 762 | ), 763 | }; 764 | return obj; 765 | }); 766 | 767 | return credit!.concat(mortgages!).concat(student!); 768 | }; 769 | 770 | export const transformSignalData = (data: SignalEvaluateResponse) => { 771 | return [ 772 | { 773 | customerInitiatedReturnRiskTier: 774 | data.scores.customer_initiated_return_risk!.risk_tier, 775 | customerInitiatedReturnRiskScore: 776 | data.scores.customer_initiated_return_risk!.score, 777 | bankInitiatedReturnRiskTier: 778 | data.scores.bank_initiated_return_risk!.risk_tier, 779 | bankInitiatedReturnRiskScore: 780 | data.scores.bank_initiated_return_risk!.score, 781 | daysSinceFirstPlaidConnection: 782 | data.core_attributes!.days_since_first_plaid_connection, 783 | }, 784 | ]; 785 | }; 786 | 787 | export const transformTransferAuthorizationData = ( 788 | data: TransferAuthorizationCreateResponse 789 | ): Array => { 790 | const transferAuthorizationData = data.authorization; 791 | return [ 792 | { 793 | authorizationId: transferAuthorizationData.id, 794 | authorizationDecision: transferAuthorizationData.decision, 795 | decisionRationaleCode: 796 | transferAuthorizationData.decision_rationale != null 797 | ? transferAuthorizationData.decision_rationale.code 798 | : "null", 799 | decisionRationaleDescription: 800 | transferAuthorizationData.decision_rationale != null 801 | ? transferAuthorizationData.decision_rationale.description 802 | : "null", 803 | }, 804 | ]; 805 | }; 806 | 807 | export const transformTransferData = ( 808 | data: TransferCreateResponse 809 | ): Array => { 810 | const transferData = data.transfer; 811 | return [ 812 | { 813 | transferId: transferData.id, 814 | amount: transferData.amount, 815 | type: transferData.type, 816 | achClass: transferData.ach_class || null, 817 | network: transferData.network, 818 | status: transferData.status, 819 | }, 820 | ]; 821 | }; 822 | 823 | interface ItemData { 824 | item: ItemGetResponse["item"]; 825 | institution: InstitutionsGetByIdResponse["institution"]; 826 | } 827 | 828 | export const transformItemData = (data: ItemData): Array => { 829 | return [ 830 | { 831 | name: data.institution.name, 832 | billed: data.item.billed_products.join(", "), 833 | available: data.item.available_products.join(", "), 834 | }, 835 | ]; 836 | }; 837 | 838 | export const transformAccountsData = (data: AccountsGetResponse) => { 839 | const accountsData = data.accounts; 840 | return accountsData.map((account) => { 841 | const balance: number | null | undefined = 842 | account.balances.available || account.balances.current; 843 | const obj: DataItem = { 844 | name: account.name, 845 | balance: formatCurrency(balance, account.balances.iso_currency_code), 846 | subtype: account.subtype, 847 | mask: account.mask!, 848 | }; 849 | return obj; 850 | }); 851 | }; 852 | 853 | interface PaymentData { 854 | payment: PaymentInitiationPaymentGetResponse; 855 | } 856 | 857 | export const transformPaymentData = (data: PaymentData) => { 858 | const statusUpdate = 859 | typeof data.payment.last_status_update === "string" 860 | ? data.payment.last_status_update.replace("T", " ").replace("Z", "") 861 | : new Date(data.payment.last_status_update * 1000) // Java data comes as timestamp 862 | .toISOString() 863 | .replace("T", " ") 864 | .replace("Z", ""); 865 | return [ 866 | { 867 | paymentId: data.payment.payment_id, 868 | amount: `${data.payment.amount.currency} ${data.payment.amount.value}`, 869 | status: data.payment.status, 870 | statusUpdate: statusUpdate, 871 | recipientId: data.payment.recipient_id, 872 | }, 873 | ]; 874 | }; 875 | 876 | interface AssetResponseData { 877 | json: AssetReport; 878 | } 879 | 880 | export const transformAssetsData = (data: AssetResponseData) => { 881 | const assetItems = data.json.items; 882 | return assetItems.flatMap((item) => { 883 | return item.accounts.map((account) => { 884 | const balance: number | null | undefined = 885 | account.balances.available || account.balances.current; 886 | const obj: DataItem = { 887 | account: account.name, 888 | balance: formatCurrency(balance, account.balances.iso_currency_code), 889 | transactions: account.transactions!.length, 890 | daysAvailable: account.days_available!, 891 | }; 892 | return obj; 893 | }); 894 | }); 895 | }; 896 | 897 | interface IncomePaystub { 898 | paystubs: IncomeVerificationPaystubsGetResponse; 899 | } 900 | 901 | export const transformIncomePaystubsData = (data: IncomePaystub) => { 902 | const paystubsItemsArray: Array = data.paystubs.paystubs; 903 | var finalArray: Array = []; 904 | for (var i = 0; i < paystubsItemsArray.length; i++) { 905 | var ActualEarningVariable: any = paystubsItemsArray[i].earnings; 906 | for (var j = 0; j < ActualEarningVariable.breakdown.length; j++) { 907 | var payStubItem: IncomePaystubsDataItem = { 908 | description: 909 | paystubsItemsArray[i].employer.name + 910 | "_" + 911 | ActualEarningVariable.breakdown[j].description, 912 | currentAmount: ActualEarningVariable.breakdown[j].current_amount, 913 | currency: ActualEarningVariable.breakdown[j].iso_currency_code, 914 | }; 915 | finalArray.push(payStubItem); 916 | } 917 | } 918 | return finalArray; 919 | }; 920 | 921 | export const transformBaseReportGetData = (data: CraCheckReportBaseReportGetResponse) => { 922 | const report = data.report; 923 | return report.items.flatMap((item) => 924 | item.accounts.map((account) => { 925 | const accountInsights = account.account_insights; 926 | const averageInflow = accountInsights?.average_inflow_amount?.pop()?.total_amount; 927 | const averageOutflow = accountInsights?.average_outflow_amount?.pop()?.total_amount; 928 | return { 929 | accountName: account.name, 930 | averageDaysBetweenTransactions: accountInsights?.average_days_between_transactions?.toFixed(2), 931 | averageInflowAmount: formatCurrency(averageInflow?.amount, averageInflow?.iso_currency_code), 932 | averageOutflowAmount: formatCurrency(averageOutflow?.amount, averageOutflow?.iso_currency_code), 933 | averageBalance: formatCurrency(account.balances.average_balance, account.balances.iso_currency_code), 934 | balance: formatCurrency(account.balances.available, account.balances.iso_currency_code) 935 | }; 936 | })) as Array; 937 | }; 938 | 939 | 940 | export const transformIncomeInsightsData = (data: CraCheckReportIncomeInsightsGetResponse) => { 941 | const report = data.report?.bank_income_summary 942 | const historicalIncome = report?.historical_annual_income?.pop() 943 | const forecastedIncome = report?.forecasted_annual_income?.pop() 944 | return [ 945 | { 946 | incomeSourcesCount: report?.income_sources_count, 947 | historicalAnnualIncome: formatCurrency(historicalIncome?.amount, historicalIncome?.iso_currency_code), 948 | forecastedAnnualIncome: formatCurrency(forecastedIncome?.amount, forecastedIncome?.iso_currency_code) 949 | } 950 | ] as Array; 951 | }; 952 | 953 | 954 | export const transformPartnerInsightsData = (data: CraCheckReportPartnerInsightsGetResponse) => { 955 | const report = data.report?.prism 956 | return [ 957 | { 958 | cashScore: report?.cash_score?.score, 959 | firstDetectScore: report?.first_detect?.score, 960 | } 961 | ] as Array; 962 | }; 963 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import { QuickstartProvider } from "./Context"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | , 13 | document.getElementById("root") 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /frontend/src/setupProxy.js: -------------------------------------------------------------------------------- 1 | const { createProxyMiddleware } = require('http-proxy-middleware'); 2 | 3 | module.exports = function(app) { 4 | app.use( 5 | '/api', 6 | createProxyMiddleware({ 7 | target: process.env.REACT_APP_API_HOST || 'http://127.0.0.1:8000', 8 | changeOrigin: true, 9 | }) 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src", "index.js"] 20 | } 21 | -------------------------------------------------------------------------------- /go/.env: -------------------------------------------------------------------------------- 1 | ../.env -------------------------------------------------------------------------------- /go/.env.example: -------------------------------------------------------------------------------- 1 | ../.env.example -------------------------------------------------------------------------------- /go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 AS build 2 | 3 | WORKDIR /opt/src 4 | COPY . . 5 | WORKDIR /opt/src/go 6 | 7 | RUN go get -d -v ./... 8 | RUN go build -o quickstart 9 | 10 | FROM gcr.io/distroless/base-debian12 11 | 12 | COPY --from=build /opt/src/go/quickstart / 13 | COPY .env /.env 14 | 15 | EXPOSE 8000 16 | ENTRYPOINT ["/quickstart"] 17 | -------------------------------------------------------------------------------- /go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plaid/quickstart 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | github.com/joho/godotenv v1.5.1 8 | github.com/plaid/plaid-go/v31 v31.0.0 9 | ) 10 | 11 | require ( 12 | github.com/bytedance/sonic v1.11.6 // indirect 13 | github.com/bytedance/sonic/loader v0.1.1 // indirect 14 | github.com/cloudwego/base64x v0.1.4 // indirect 15 | github.com/cloudwego/iasm v0.2.0 // indirect 16 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 17 | github.com/gin-contrib/sse v0.1.0 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/go-playground/validator/v10 v10.20.0 // indirect 21 | github.com/goccy/go-json v0.10.2 // indirect 22 | github.com/golang/protobuf v1.5.2 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 25 | github.com/leodido/go-urn v1.4.0 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 28 | github.com/modern-go/reflect2 v1.0.2 // indirect 29 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 30 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 31 | github.com/ugorji/go/codec v1.2.12 // indirect 32 | golang.org/x/arch v0.8.0 // indirect 33 | golang.org/x/crypto v0.31.0 // indirect 34 | golang.org/x/net v0.25.0 // indirect 35 | golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 // indirect 36 | golang.org/x/sys v0.28.0 // indirect 37 | golang.org/x/text v0.21.0 // indirect 38 | google.golang.org/appengine v1.6.7 // indirect 39 | google.golang.org/protobuf v1.34.1 // indirect 40 | gopkg.in/yaml.v3 v3.0.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /go/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | go run server.go -------------------------------------------------------------------------------- /java/.env: -------------------------------------------------------------------------------- 1 | ../.env -------------------------------------------------------------------------------- /java/.env.example: -------------------------------------------------------------------------------- 1 | ../.env.example -------------------------------------------------------------------------------- /java/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn 10 | 11 | #java 12 | .classpath 13 | .project 14 | .settings 15 | -------------------------------------------------------------------------------- /java/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:openjdk 2 | 3 | WORKDIR /opt/app 4 | COPY . . 5 | WORKDIR /opt/app/java 6 | 7 | RUN mvn clean package 8 | 9 | EXPOSE 8000 10 | ENTRYPOINT ["java"] 11 | CMD ["-jar", "target/quickstart-1.0-SNAPSHOT.jar", "server", "config.yml"] 12 | -------------------------------------------------------------------------------- /java/config.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: INFO 3 | loggers: 4 | com.plaid: DEBUG 5 | 6 | plaid_client_id: ${PLAID_CLIENT_ID} 7 | plaid_secret: ${PLAID_SECRET} 8 | plaid_products: ${PLAID_PRODUCTS} 9 | plaid_country_codes: ${PLAID_COUNTRY_CODES} 10 | plaid_redirect_uri: ${PLAID_REDIRECT_URI:-""} 11 | plaid_env: ${PLAID_ENV:-sandbox} 12 | 13 | server: 14 | application_connectors: 15 | - type: http 16 | port: 8000 17 | admin_connectors: 18 | - type: http 19 | port: 8001 20 | rootPath: '/api' 21 | -------------------------------------------------------------------------------- /java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 4.0.0 7 | 8 | 3.0.0 9 | 10 | com.plaid 11 | quickstart 12 | 1.0-SNAPSHOT 13 | jar 14 | Quickstart 15 | 16 | 1.8 17 | 1.8 18 | UTF-8 19 | UTF-8 20 | 1.3.2 21 | com.plaid.quickstart.QuickstartApplication 22 | 23 | 24 | 25 | 26 | io.dropwizard 27 | dropwizard-bom 28 | ${dropwizard.version} 29 | pom 30 | import 31 | 32 | 33 | 34 | 35 | 36 | io.dropwizard 37 | dropwizard-core 38 | 39 | 40 | io.dropwizard 41 | dropwizard-assets 42 | ${dropwizard.version} 43 | 44 | 45 | io.dropwizard 46 | dropwizard-views 47 | ${dropwizard.version} 48 | 49 | 50 | io.dropwizard 51 | dropwizard-views-freemarker 52 | ${dropwizard.version} 53 | 54 | 55 | com.plaid 56 | plaid-java 57 | 29.0.0 58 | 59 | 60 | javax.xml.bind 61 | jaxb-api 62 | 2.3.0 63 | 64 | 65 | com.sun.xml.bind 66 | jaxb-core 67 | 2.3.0 68 | 69 | 70 | com.sun.xml.bind 71 | jaxb-impl 72 | 2.3.0 73 | 74 | 75 | com.squareup.okhttp3 76 | okhttp 77 | 4.12.0 78 | 79 | 80 | org.jetbrains.kotlin 81 | kotlin-stdlib 82 | 1.9.25 83 | 84 | 85 | 86 | javax.activation 87 | activation 88 | 1.1.1 89 | 90 | 91 | 92 | 93 | 94 | maven-shade-plugin 95 | 2.4.3 96 | 97 | true 98 | 99 | 100 | 101 | ${mainClass} 102 | 103 | 104 | 105 | 106 | 107 | package 108 | 109 | shade 110 | 111 | 112 | 113 | 114 | 115 | maven-jar-plugin 116 | 3.0.2 117 | 118 | 119 | 120 | true 121 | ${mainClass} 122 | 123 | 124 | 125 | 126 | 127 | maven-compiler-plugin 128 | 3.6.1 129 | 130 | 1.8 131 | 1.8 132 | 133 | 134 | 135 | maven-source-plugin 136 | 3.0.1 137 | 138 | 139 | attach-sources 140 | 141 | jar 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | maven-project-info-reports-plugin 152 | 2.8.1 153 | 154 | false 155 | false 156 | 157 | 158 | 159 | maven-javadoc-plugin 160 | 2.10.3 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/QuickstartApplication.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 4 | import com.plaid.client.ApiClient; 5 | import com.plaid.client.request.PlaidApi; 6 | import com.plaid.quickstart.resources.AccessTokenResource; 7 | import com.plaid.quickstart.resources.AccountsResource; 8 | import com.plaid.quickstart.resources.AssetsResource; 9 | import com.plaid.quickstart.resources.AuthResource; 10 | import com.plaid.quickstart.resources.BalanceResource; 11 | import com.plaid.quickstart.resources.CraResource; 12 | import com.plaid.quickstart.resources.HoldingsResource; 13 | import com.plaid.quickstart.resources.IdentityResource; 14 | import com.plaid.quickstart.resources.InfoResource; 15 | import com.plaid.quickstart.resources.InvestmentTransactionsResource; 16 | import com.plaid.quickstart.resources.ItemResource; 17 | import com.plaid.quickstart.resources.LinkTokenResource; 18 | import com.plaid.quickstart.resources.LinkTokenWithPaymentResource; 19 | import com.plaid.quickstart.resources.PaymentInitiationResource; 20 | import com.plaid.quickstart.resources.PublicTokenResource; 21 | import com.plaid.quickstart.resources.SignalResource; 22 | import com.plaid.quickstart.resources.StatementsResource; 23 | import com.plaid.quickstart.resources.TransactionsResource; 24 | import com.plaid.quickstart.resources.TransferAuthorizeResource; 25 | import com.plaid.quickstart.resources.TransferCreateResource; 26 | import com.plaid.quickstart.resources.UserTokenResource; 27 | import io.dropwizard.Application; 28 | import io.dropwizard.configuration.EnvironmentVariableSubstitutor; 29 | import io.dropwizard.configuration.SubstitutingSourceProvider; 30 | import io.dropwizard.setup.Bootstrap; 31 | import io.dropwizard.setup.Environment; 32 | 33 | import java.util.Arrays; 34 | import java.util.HashMap; 35 | import java.util.List; 36 | 37 | public class QuickstartApplication extends Application { 38 | // We store the accessToken in memory - in production, store it in a secure 39 | // persistent data store. 40 | public static String accessToken; 41 | public static String userToken; 42 | public static String itemID; 43 | // The paymentId is only relevant for the UK Payment Initiation product. 44 | // We store the paymentId in memory - in production, store it in a secure 45 | // persistent data store. 46 | public static String paymentId; 47 | // The authorizationId is only relevant for Transfer ACH product. 48 | // We store the transferId in memory - in production, store it in a secure 49 | // persistent data store. 50 | public static String authorizationId; 51 | public static String accountId; 52 | 53 | private PlaidApi plaidClient; 54 | private ApiClient apiClient; 55 | public String plaidEnv; 56 | 57 | public static void main(final String[] args) throws Exception { 58 | new QuickstartApplication().run(args); 59 | } 60 | 61 | @Override 62 | public String getName() { 63 | return "Quickstart"; 64 | } 65 | 66 | @Override 67 | public void initialize(final Bootstrap bootstrap) { 68 | bootstrap.getObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); 69 | bootstrap.setConfigurationSourceProvider( 70 | new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(), 71 | new EnvironmentVariableSubstitutor(false) 72 | ) 73 | ); 74 | } 75 | 76 | @Override 77 | public void run(final QuickstartConfiguration configuration, 78 | final Environment environment) { 79 | // or equivalent, depending on which environment you're calling into 80 | switch (configuration.getPlaidEnv()) { 81 | case "sandbox": 82 | plaidEnv = ApiClient.Sandbox; 83 | break; 84 | case "production": 85 | plaidEnv = ApiClient.Production; 86 | break; 87 | default: 88 | plaidEnv = ApiClient.Sandbox; 89 | } 90 | List plaidProducts = Arrays.asList(configuration.getPlaidProducts().split(",")); 91 | List countryCodes = Arrays.asList(configuration.getPlaidCountryCodes().split(",")); 92 | String plaidClientId = System.getenv("PLAID_CLIENT_ID"); 93 | String plaidSecret = System.getenv("PLAID_SECRET"); 94 | String redirectUri = null; 95 | if (configuration.getPlaidRedirectUri() != null && configuration.getPlaidRedirectUri().length() > 0) { 96 | redirectUri = configuration.getPlaidRedirectUri(); 97 | } 98 | 99 | HashMap apiKeys = new HashMap(); 100 | apiKeys.put("clientId", plaidClientId); 101 | apiKeys.put("secret", plaidSecret); 102 | apiKeys.put("plaidVersion", "2020-09-14"); 103 | apiClient = new ApiClient(apiKeys); 104 | apiClient.setPlaidAdapter(plaidEnv); 105 | 106 | plaidClient = apiClient.createService(PlaidApi.class); 107 | 108 | environment.jersey().register(new AccessTokenResource(plaidClient, plaidProducts)); 109 | environment.jersey().register(new AccountsResource(plaidClient)); 110 | environment.jersey().register(new AssetsResource(plaidClient)); 111 | environment.jersey().register(new AuthResource(plaidClient)); 112 | environment.jersey().register(new BalanceResource(plaidClient)); 113 | environment.jersey().register(new HoldingsResource(plaidClient)); 114 | environment.jersey().register(new IdentityResource(plaidClient)); 115 | environment.jersey().register(new InfoResource(plaidProducts)); 116 | environment.jersey().register(new InvestmentTransactionsResource(plaidClient)); 117 | environment.jersey().register(new ItemResource(plaidClient)); 118 | environment.jersey().register(new LinkTokenResource(plaidClient, plaidProducts, countryCodes, redirectUri)); 119 | environment.jersey().register(new LinkTokenWithPaymentResource(plaidClient, plaidProducts, countryCodes, redirectUri)); 120 | environment.jersey().register(new PaymentInitiationResource(plaidClient)); 121 | environment.jersey().register(new PublicTokenResource(plaidClient)); 122 | environment.jersey().register(new SignalResource(plaidClient)); 123 | environment.jersey().register(new StatementsResource(plaidClient)); 124 | environment.jersey().register(new TransactionsResource(plaidClient)); 125 | environment.jersey().register(new TransferAuthorizeResource(plaidClient)); 126 | environment.jersey().register(new TransferCreateResource(plaidClient)); 127 | environment.jersey().register(new UserTokenResource(plaidClient, plaidProducts)); 128 | environment.jersey().register(new CraResource(plaidClient)); 129 | } 130 | 131 | protected PlaidApi client() { 132 | return plaidClient; 133 | } 134 | 135 | protected ApiClient apiClient() { 136 | return apiClient; 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/QuickstartConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart; 2 | 3 | import io.dropwizard.Configuration; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import org.hibernate.validator.constraints.*; 6 | 7 | public class QuickstartConfiguration extends Configuration { 8 | @NotEmpty 9 | private String plaidClientID; 10 | 11 | @NotEmpty 12 | private String plaidSecret; 13 | 14 | @NotEmpty 15 | private String plaidEnv; 16 | 17 | @NotEmpty 18 | private String plaidProducts; 19 | 20 | @NotEmpty 21 | private String plaidCountryCodes; 22 | 23 | // Parameters used for the OAuth redirect Link flow. 24 | 25 | // Set PLAID_REDIRECT_URI to 'http://localhost:3000/' 26 | // The OAuth redirect flow requires an endpoint on the developer's website 27 | // that the bank website should redirect to. You will need to configure 28 | // this redirect URI for your client ID through the Plaid developer dashboard 29 | // at https://dashboard.plaid.com/team/api. 30 | private String plaidRedirectUri; 31 | 32 | @JsonProperty 33 | public String getPlaidClientID() { 34 | return plaidClientID; 35 | } 36 | 37 | @JsonProperty 38 | public String getPlaidSecret() { 39 | return plaidSecret; 40 | } 41 | 42 | @JsonProperty 43 | public String getPlaidEnv() { 44 | return plaidEnv; 45 | } 46 | 47 | @JsonProperty 48 | public String getPlaidProducts() { 49 | return plaidProducts; 50 | } 51 | 52 | @JsonProperty 53 | public String getPlaidCountryCodes() { 54 | return plaidCountryCodes; 55 | } 56 | 57 | @JsonProperty 58 | public String getPlaidRedirectUri() { 59 | return plaidRedirectUri; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/AccessTokenResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.plaid.client.request.PlaidApi; 6 | import com.plaid.client.model.ItemPublicTokenExchangeRequest; 7 | import com.plaid.client.model.ItemPublicTokenExchangeResponse; 8 | import com.plaid.quickstart.QuickstartApplication; 9 | import com.plaid.client.model.Products; 10 | 11 | import java.util.List; 12 | import java.util.Arrays; 13 | import javax.ws.rs.POST; 14 | import javax.ws.rs.Path; 15 | import javax.ws.rs.Produces; 16 | import javax.ws.rs.FormParam; 17 | import javax.ws.rs.core.MediaType; 18 | 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import retrofit2.Response; 23 | 24 | @Path("/set_access_token") 25 | @Produces(MediaType.APPLICATION_JSON) 26 | public class AccessTokenResource { 27 | private static final Logger LOG = LoggerFactory.getLogger(AccessTokenResource.class); 28 | private final PlaidApi plaidClient; 29 | private final List plaidProducts; 30 | 31 | public AccessTokenResource(PlaidApi plaidClient, List plaidProducts) { 32 | this.plaidClient = plaidClient; 33 | this.plaidProducts = plaidProducts; 34 | } 35 | 36 | @POST 37 | public InfoResource.InfoResponse getAccessToken(@FormParam("public_token") String publicToken) 38 | throws IOException { 39 | ItemPublicTokenExchangeRequest request = new ItemPublicTokenExchangeRequest() 40 | .publicToken(publicToken); 41 | 42 | Response response = plaidClient 43 | .itemPublicTokenExchange(request) 44 | .execute(); 45 | 46 | // Ideally, we would store this somewhere more persistent 47 | QuickstartApplication. 48 | accessToken = response.body().getAccessToken(); 49 | QuickstartApplication.itemID = response.body().getItemId(); 50 | LOG.info("public token: " + publicToken); 51 | LOG.info("access token: " + QuickstartApplication.accessToken); 52 | LOG.info("item ID: " + response.body().getItemId()); 53 | return new InfoResource.InfoResponse(Arrays.asList(), QuickstartApplication.accessToken, 54 | QuickstartApplication.itemID); 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/AccountsResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.plaid.client.request.PlaidApi; 6 | import com.plaid.client.model.AccountsGetRequest; 7 | import com.plaid.client.model.AccountsGetResponse; 8 | import com.plaid.quickstart.QuickstartApplication; 9 | 10 | import javax.ws.rs.GET; 11 | import javax.ws.rs.Path; 12 | import javax.ws.rs.Produces; 13 | import javax.ws.rs.core.MediaType; 14 | 15 | import retrofit2.Response; 16 | 17 | @Path("/accounts") 18 | @Produces(MediaType.APPLICATION_JSON) 19 | public class AccountsResource { 20 | private final PlaidApi plaidClient; 21 | 22 | public AccountsResource(PlaidApi plaidClient) { 23 | this.plaidClient = plaidClient; 24 | } 25 | 26 | @GET 27 | public AccountsGetResponse getAccounts() throws IOException { 28 | AccountsGetRequest request = new AccountsGetRequest() 29 | .accessToken(QuickstartApplication.accessToken); 30 | 31 | Response response = plaidClient 32 | .accountsGet(request) 33 | .execute(); 34 | return response.body(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/AssetsResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.plaid.client.request.PlaidApi; 7 | import com.plaid.client.model.AssetReportCreateRequest; 8 | import com.plaid.client.model.AssetReportCreateResponse; 9 | import com.plaid.client.model.AssetReportGetRequest; 10 | import com.plaid.client.model.AssetReportGetResponse; 11 | import com.plaid.client.model.AssetReportPDFGetRequest; 12 | import com.plaid.quickstart.QuickstartApplication; 13 | import com.plaid.client.model.PlaidError; 14 | import com.plaid.client.model.PlaidErrorType; 15 | import okhttp3.ResponseBody; 16 | 17 | import java.util.List; 18 | import java.util.ArrayList; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | import java.util.Base64; 22 | import javax.ws.rs.GET; 23 | import javax.ws.rs.Path; 24 | import javax.ws.rs.Produces; 25 | import javax.ws.rs.core.MediaType; 26 | 27 | import retrofit2.Response; 28 | import com.google.gson.Gson; 29 | import jersey.repackaged.com.google.common.base.Throwables; 30 | 31 | @Path("/assets") 32 | @Produces(MediaType.APPLICATION_JSON) 33 | public class AssetsResource { 34 | private final PlaidApi plaidClient; 35 | 36 | public AssetsResource(PlaidApi plaidClient) { 37 | this.plaidClient = plaidClient; 38 | } 39 | 40 | @GET 41 | public Map getAssetReport() throws IOException { 42 | ArrayList accessTokens = new ArrayList<>(); 43 | accessTokens.add(QuickstartApplication.accessToken); 44 | 45 | AssetReportCreateRequest assetReportCreateRequest = new AssetReportCreateRequest() 46 | .accessTokens(accessTokens) 47 | .daysRequested(10); 48 | 49 | Response assetReportCreateResponse = plaidClient 50 | .assetReportCreate(assetReportCreateRequest) 51 | .execute(); 52 | 53 | String assetReportToken = assetReportCreateResponse.body().getAssetReportToken(); 54 | AssetReportGetRequest assetReportGetRequest = new AssetReportGetRequest() 55 | .assetReportToken(assetReportToken); 56 | Response assetReportGetResponse = null; 57 | 58 | //In a real integration, we would wait for a webhook rather than polling like this 59 | for (int i = 0; i < 5; i++){ 60 | assetReportGetResponse = plaidClient.assetReportGet(assetReportGetRequest).execute(); 61 | if (assetReportGetResponse.isSuccessful()){ 62 | break; 63 | } else { 64 | try { 65 | Gson gson = new Gson(); 66 | PlaidError error = gson.fromJson(assetReportGetResponse.errorBody().string(), PlaidError.class); 67 | error.getErrorType().equals(PlaidErrorType.ASSET_REPORT_ERROR); 68 | error.getErrorCode().equals("PRODUCT_NOT_READY"); 69 | Thread.sleep(5000); 70 | } catch (Exception e) { 71 | throw Throwables.propagate(e); 72 | } 73 | } 74 | } 75 | 76 | AssetReportPDFGetRequest assetReportPDFGetRequest = new AssetReportPDFGetRequest() 77 | .assetReportToken(assetReportToken); 78 | 79 | Response assetReportPDFGetResponse = plaidClient 80 | .assetReportPdfGet(assetReportPDFGetRequest) 81 | .execute(); 82 | 83 | String pdf = Base64.getEncoder().encodeToString(assetReportPDFGetResponse.body().bytes()); 84 | Map responseMap = new HashMap<>(); 85 | responseMap.put("json", assetReportGetResponse.body().getReport()); 86 | responseMap.put("pdf", pdf); 87 | return responseMap; 88 | 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/AuthResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.plaid.client.request.PlaidApi; 6 | import com.plaid.client.model.AuthGetRequest; 7 | import com.plaid.client.model.AuthGetResponse; 8 | import com.plaid.quickstart.QuickstartApplication; 9 | 10 | import javax.ws.rs.GET; 11 | import javax.ws.rs.Path; 12 | import javax.ws.rs.Produces; 13 | import javax.ws.rs.core.MediaType; 14 | 15 | import retrofit2.Response; 16 | 17 | @Path("/auth") 18 | @Produces(MediaType.APPLICATION_JSON) 19 | public class AuthResource { 20 | private final PlaidApi plaidClient; 21 | 22 | public AuthResource(PlaidApi plaidClient) { 23 | this.plaidClient = plaidClient; 24 | } 25 | 26 | @GET 27 | public AuthGetResponse getAccounts() throws IOException { 28 | 29 | AuthGetRequest request = new AuthGetRequest() 30 | .accessToken(QuickstartApplication.accessToken); 31 | Response response = plaidClient 32 | .authGet(request) 33 | .execute(); 34 | 35 | return response.body(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/BalanceResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.plaid.client.request.PlaidApi; 6 | import com.plaid.client.model.AccountsBalanceGetRequest; 7 | import com.plaid.client.model.AccountsGetResponse; 8 | import com.plaid.quickstart.QuickstartApplication; 9 | 10 | import javax.ws.rs.GET; 11 | import javax.ws.rs.Path; 12 | import javax.ws.rs.Produces; 13 | import javax.ws.rs.core.MediaType; 14 | 15 | import retrofit2.Response; 16 | 17 | @Path("/balance") 18 | @Produces(MediaType.APPLICATION_JSON) 19 | public class BalanceResource { 20 | private final PlaidApi plaidClient; 21 | 22 | public BalanceResource(PlaidApi plaidClient) { 23 | this.plaidClient = plaidClient; 24 | } 25 | 26 | @GET 27 | public AccountsGetResponse getAccounts() throws IOException { 28 | AccountsBalanceGetRequest request = new AccountsBalanceGetRequest() 29 | .accessToken(QuickstartApplication.accessToken); 30 | 31 | Response response = plaidClient 32 | .accountsBalanceGet(request) 33 | .execute(); 34 | return response.body(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/CraResource.java: -------------------------------------------------------------------------------- 1 | 2 | package com.plaid.quickstart.resources; 3 | 4 | import com.plaid.client.model.CraCheckReportBaseReportGetRequest; 5 | import com.plaid.client.model.CraCheckReportBaseReportGetResponse; 6 | import com.plaid.client.model.CraCheckReportIncomeInsightsGetRequest; 7 | import com.plaid.client.model.CraCheckReportIncomeInsightsGetResponse; 8 | import com.plaid.client.model.CraCheckReportPDFGetRequest; 9 | import com.plaid.client.model.CraCheckReportPartnerInsightsGetRequest; 10 | import com.plaid.client.model.CraCheckReportPartnerInsightsGetResponse; 11 | import com.plaid.client.model.CraPDFAddOns; 12 | import com.plaid.client.request.PlaidApi; 13 | import com.plaid.quickstart.QuickstartApplication; 14 | 15 | import jersey.repackaged.com.google.common.base.Throwables; 16 | import okhttp3.ResponseBody; 17 | import retrofit2.Call; 18 | import retrofit2.Response; 19 | 20 | import javax.ws.rs.GET; 21 | import javax.ws.rs.Path; 22 | import javax.ws.rs.Produces; 23 | import javax.ws.rs.core.MediaType; 24 | import java.io.IOException; 25 | import java.util.Base64; 26 | import java.util.HashMap; 27 | import java.util.Map; 28 | 29 | @Produces(MediaType.APPLICATION_JSON) 30 | @Path("/cra") 31 | public class CraResource { 32 | private final PlaidApi plaidClient; 33 | 34 | public CraResource(PlaidApi plaidClient) { 35 | this.plaidClient = plaidClient; 36 | } 37 | 38 | // Retrieve CRA Base Report and PDF 39 | // Base report: https://plaid.com/docs/check/api/#cracheck_reportbase_reportget 40 | // PDF: https://plaid.com/docs/check/api/#cracheck_reportpdfget 41 | @GET 42 | @Path("/get_base_report") 43 | public Map getBaseReport() throws IOException { 44 | CraCheckReportBaseReportGetRequest request = new CraCheckReportBaseReportGetRequest(); 45 | request.setUserToken(QuickstartApplication.userToken); 46 | CraCheckReportBaseReportGetResponse baseReportResponse = pollWithRetries( 47 | plaidClient.craCheckReportBaseReportGet(request) 48 | ).body(); 49 | 50 | CraCheckReportPDFGetRequest pdfRequest = new CraCheckReportPDFGetRequest(); 51 | pdfRequest.setUserToken(QuickstartApplication.userToken); 52 | Response pdfResponse = plaidClient.craCheckReportPdfGet(pdfRequest).execute(); 53 | 54 | String pdfBase64 = Base64.getEncoder().encodeToString(pdfResponse.body().bytes()); 55 | 56 | Map responseMap = new HashMap<>(); 57 | responseMap.put("report", baseReportResponse.getReport()); 58 | responseMap.put("pdf", pdfBase64); 59 | return responseMap; 60 | } 61 | 62 | // Retrieve CRA Income Insights and PDF with Insights 63 | // Income insights: 64 | // https://plaid.com/docs/check/api/#cracheck_reportincome_insightsget 65 | // PDF w/ income insights: 66 | // https://plaid.com/docs/check/api/#cracheck_reportpdfget 67 | @GET 68 | @Path("/get_income_insights") 69 | public Map getIncomeInsigts() throws IOException { 70 | CraCheckReportIncomeInsightsGetRequest request = new CraCheckReportIncomeInsightsGetRequest(); 71 | request.setUserToken(QuickstartApplication.userToken); 72 | CraCheckReportIncomeInsightsGetResponse baseReportResponse = pollWithRetries( 73 | plaidClient.craCheckReportIncomeInsightsGet(request) 74 | ).body(); 75 | 76 | CraCheckReportPDFGetRequest pdfRequest = new CraCheckReportPDFGetRequest(); 77 | pdfRequest.setUserToken(QuickstartApplication.userToken); 78 | pdfRequest.addAddOnsItem(CraPDFAddOns.CRA_INCOME_INSIGHTS); 79 | Response pdfResponse = plaidClient.craCheckReportPdfGet(pdfRequest).execute(); 80 | 81 | String pdfBase64 = Base64.getEncoder().encodeToString(pdfResponse.body().bytes()); 82 | 83 | Map responseMap = new HashMap<>(); 84 | responseMap.put("report", baseReportResponse.getReport()); 85 | responseMap.put("pdf", pdfBase64); 86 | return responseMap; 87 | } 88 | 89 | // Retrieve CRA Partner Insights 90 | // https://plaid.com/docs/check/api/#cracheck_reportpartner_insightsget 91 | @GET 92 | @Path("/get_partner_insights") 93 | public CraCheckReportPartnerInsightsGetResponse getPartnerInsigts() throws IOException { 94 | CraCheckReportPartnerInsightsGetRequest request = new CraCheckReportPartnerInsightsGetRequest(); 95 | request.setUserToken(QuickstartApplication.userToken); 96 | return pollWithRetries(plaidClient.craCheckReportPartnerInsightsGet(request)).body(); 97 | } 98 | 99 | // Since this quickstart does not support webhooks, this function can be used to 100 | // poll 101 | // an API that would otherwise be triggered by a webhook. 102 | // For a webhook example, see 103 | // https://github.com/plaid/tutorial-resources or 104 | // https://github.com/plaid/pattern 105 | private Response pollWithRetries(Call requestCallback) throws IOException { 106 | for (int i = 0; i <= 20; i++) { 107 | Response response = requestCallback.execute(); 108 | 109 | if (response.isSuccessful()) { 110 | return response; 111 | } else { 112 | try { 113 | Thread.sleep(5000); 114 | } catch (Exception e) { 115 | throw Throwables.propagate(e); 116 | } 117 | } 118 | 119 | } 120 | throw Throwables.propagate(new Exception("Ran out of retries while polling")); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/HoldingsResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | import com.plaid.client.request.PlaidApi; 8 | import com.plaid.client.model.InvestmentsHoldingsGetRequest; 9 | import com.plaid.client.model.InvestmentsHoldingsGetResponse; 10 | import com.plaid.quickstart.QuickstartApplication; 11 | 12 | import javax.ws.rs.GET; 13 | import javax.ws.rs.Path; 14 | import javax.ws.rs.Produces; 15 | import javax.ws.rs.core.MediaType; 16 | 17 | import retrofit2.Response; 18 | 19 | @Path("/holdings") 20 | @Produces(MediaType.APPLICATION_JSON) 21 | public class HoldingsResource { 22 | private final PlaidApi plaidClient; 23 | 24 | public HoldingsResource(PlaidApi plaidClient) { 25 | this.plaidClient = plaidClient; 26 | } 27 | 28 | @GET 29 | public HoldingsResponse getAccounts() throws IOException { 30 | 31 | InvestmentsHoldingsGetRequest request = new InvestmentsHoldingsGetRequest() 32 | .accessToken(QuickstartApplication.accessToken); 33 | 34 | Response response = plaidClient 35 | .investmentsHoldingsGet(request) 36 | .execute(); 37 | 38 | return new HoldingsResponse(response.body()); 39 | } 40 | 41 | private static class HoldingsResponse { 42 | @JsonProperty 43 | private final InvestmentsHoldingsGetResponse holdings; 44 | 45 | public HoldingsResponse(InvestmentsHoldingsGetResponse response) { 46 | this.holdings = response; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/IdentityResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.plaid.client.request.PlaidApi; 7 | import com.plaid.client.model.AccountIdentity; 8 | import com.plaid.client.model.IdentityGetRequest; 9 | import com.plaid.client.model.IdentityGetResponse; 10 | import com.plaid.quickstart.QuickstartApplication; 11 | 12 | import java.util.List; 13 | import javax.ws.rs.GET; 14 | import javax.ws.rs.Path; 15 | import javax.ws.rs.Produces; 16 | import javax.ws.rs.core.MediaType; 17 | 18 | import retrofit2.Response; 19 | 20 | @Path("/identity") 21 | @Produces(MediaType.APPLICATION_JSON) 22 | public class IdentityResource { 23 | private final PlaidApi plaidClient; 24 | 25 | public IdentityResource(PlaidApi plaidClient) { 26 | this.plaidClient = plaidClient; 27 | } 28 | 29 | @GET 30 | public IdentityResponse getAccounts() throws IOException { 31 | IdentityGetRequest request = new IdentityGetRequest() 32 | .accessToken(QuickstartApplication.accessToken); 33 | Response response = plaidClient 34 | .identityGet(request) 35 | .execute(); 36 | return new IdentityResponse(response.body()); 37 | } 38 | 39 | private static class IdentityResponse { 40 | @JsonProperty 41 | private final List identity; 42 | 43 | public IdentityResponse(IdentityGetResponse response) { 44 | this.identity = response.getAccounts(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/InfoResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import com.plaid.quickstart.QuickstartApplication; 6 | 7 | import java.util.List; 8 | import javax.ws.rs.POST; 9 | import javax.ws.rs.Path; 10 | import javax.ws.rs.Produces; 11 | import javax.ws.rs.core.MediaType; 12 | 13 | @Path("/info") 14 | @Produces(MediaType.APPLICATION_JSON) 15 | public class InfoResource { 16 | private final List plaidProducts; 17 | 18 | public InfoResource(List plaidProducts) { 19 | this.plaidProducts = plaidProducts; 20 | } 21 | 22 | public static class InfoResponse { 23 | @JsonProperty 24 | private final String itemId; 25 | @JsonProperty 26 | private final String accessToken; 27 | @JsonProperty 28 | private final List products; 29 | 30 | public InfoResponse(List plaidProducts, String accessToken, String itemID) { 31 | this.products = plaidProducts; 32 | this.accessToken = accessToken; 33 | this.itemId = itemID; 34 | } 35 | } 36 | 37 | @POST 38 | public InfoResponse getInfo() { 39 | return new InfoResponse(plaidProducts, QuickstartApplication.accessToken, 40 | QuickstartApplication.itemID); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/InvestmentTransactionsResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | 6 | import com.plaid.client.request.PlaidApi; 7 | import com.plaid.client.model.InvestmentsTransactionsGetRequest; 8 | import com.plaid.client.model.InvestmentsTransactionsGetResponse; 9 | import com.plaid.client.model.InvestmentsTransactionsGetRequestOptions; 10 | import com.plaid.quickstart.QuickstartApplication; 11 | 12 | import java.io.IOException; 13 | import java.time.LocalDate; 14 | import javax.ws.rs.GET; 15 | import javax.ws.rs.Path; 16 | import javax.ws.rs.Produces; 17 | import javax.ws.rs.core.MediaType; 18 | 19 | import retrofit2.Response; 20 | 21 | @Path("/investments_transactions") 22 | @Produces(MediaType.APPLICATION_JSON) 23 | public class InvestmentTransactionsResource { 24 | private final PlaidApi plaidClient; 25 | 26 | public InvestmentTransactionsResource(PlaidApi plaidClient) { 27 | this.plaidClient = plaidClient; 28 | } 29 | 30 | @GET 31 | public InvestmentTransactionsResponse getAccounts() throws IOException { 32 | LocalDate startDate = LocalDate.now().minusDays(30); 33 | LocalDate endDate = LocalDate.now(); 34 | InvestmentsTransactionsGetRequestOptions options = new InvestmentsTransactionsGetRequestOptions() 35 | .count(100); 36 | 37 | InvestmentsTransactionsGetRequest request = new InvestmentsTransactionsGetRequest() 38 | .accessToken(QuickstartApplication.accessToken) 39 | .startDate(startDate) 40 | .endDate(endDate) 41 | .options(options); 42 | 43 | Response response = plaidClient 44 | .investmentsTransactionsGet(request) 45 | .execute(); 46 | return new InvestmentTransactionsResponse(response.body()); 47 | } 48 | 49 | private static class InvestmentTransactionsResponse { 50 | @JsonProperty 51 | private InvestmentsTransactionsGetResponse investmentsTransactions; 52 | 53 | public InvestmentTransactionsResponse(InvestmentsTransactionsGetResponse investmentTransactions) { 54 | this.investmentsTransactions = investmentTransactions; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/ItemResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | import com.plaid.client.request.PlaidApi; 8 | import com.plaid.client.model.CountryCode; 9 | import com.plaid.client.model.ItemGetRequest; 10 | import com.plaid.client.model.ItemGetResponse; 11 | import com.plaid.client.model.InstitutionsGetByIdRequest; 12 | import com.plaid.client.model.InstitutionsGetByIdResponse; 13 | import com.plaid.client.model.Institution; 14 | import com.plaid.client.model.ItemWithConsentFields; 15 | import com.plaid.quickstart.QuickstartApplication; 16 | 17 | import javax.ws.rs.GET; 18 | import javax.ws.rs.Path; 19 | import javax.ws.rs.Produces; 20 | import javax.ws.rs.core.MediaType; 21 | 22 | import retrofit2.Response; 23 | 24 | @Path("/item") 25 | @Produces(MediaType.APPLICATION_JSON) 26 | public class ItemResource { 27 | private final PlaidApi plaidClient; 28 | 29 | public ItemResource(PlaidApi plaidClient) { 30 | this.plaidClient = plaidClient; 31 | } 32 | 33 | @GET 34 | public ItemResponse getItem() throws IOException { 35 | ItemGetRequest request = new ItemGetRequest() 36 | .accessToken(QuickstartApplication.accessToken); 37 | 38 | Response itemResponse = plaidClient 39 | .itemGet(request) 40 | .execute(); 41 | 42 | InstitutionsGetByIdRequest institutionsRequest = new InstitutionsGetByIdRequest() 43 | .institutionId(itemResponse.body().getItem().getInstitutionId()) 44 | .addCountryCodesItem(CountryCode.US); 45 | 46 | Response institutionsResponse = plaidClient 47 | .institutionsGetById(institutionsRequest) 48 | .execute(); 49 | 50 | return new ItemResponse( 51 | itemResponse.body().getItem(), 52 | institutionsResponse.body().getInstitution() 53 | ); 54 | } 55 | 56 | public static class ItemResponse { 57 | @JsonProperty 58 | public ItemWithConsentFields item; 59 | 60 | @JsonProperty 61 | public Institution institution; 62 | 63 | public ItemResponse(ItemWithConsentFields item, Institution institution) { 64 | this.item = item; 65 | this.institution = institution; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/LinkTokenResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.plaid.client.model.ConsumerReportPermissiblePurpose; 5 | import com.plaid.client.model.CountryCode; 6 | import com.plaid.client.model.LinkTokenCreateRequest; 7 | import com.plaid.client.model.LinkTokenCreateRequestCraOptions; 8 | import com.plaid.client.model.LinkTokenCreateRequestStatements; 9 | import com.plaid.client.model.LinkTokenCreateRequestUser; 10 | import com.plaid.client.model.LinkTokenCreateResponse; 11 | import com.plaid.client.model.Products; 12 | import com.plaid.client.request.PlaidApi; 13 | import com.plaid.quickstart.QuickstartApplication; 14 | import retrofit2.Response; 15 | 16 | import javax.ws.rs.POST; 17 | import javax.ws.rs.Path; 18 | import javax.ws.rs.Produces; 19 | import javax.ws.rs.core.MediaType; 20 | import java.io.IOException; 21 | import java.time.LocalDate; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.Date; 25 | import java.util.List; 26 | 27 | @Path("/create_link_token") 28 | @Produces(MediaType.APPLICATION_JSON) 29 | public class LinkTokenResource { 30 | private final PlaidApi plaidClient; 31 | private final List plaidProducts; 32 | private final List countryCodes; 33 | private final String redirectUri; 34 | private final List correctedPlaidProducts; 35 | private final List correctedCountryCodes; 36 | 37 | public LinkTokenResource(PlaidApi plaidClient, List plaidProducts, 38 | List countryCodes, String redirectUri) { 39 | this.plaidClient = plaidClient; 40 | this.plaidProducts = plaidProducts; 41 | this.countryCodes = countryCodes; 42 | this.redirectUri = redirectUri; 43 | this.correctedPlaidProducts = new ArrayList<>(); 44 | this.correctedCountryCodes = new ArrayList<>(); 45 | } 46 | 47 | public static class LinkToken { 48 | @JsonProperty 49 | private String linkToken; 50 | 51 | 52 | public LinkToken(String linkToken) { 53 | this.linkToken = linkToken; 54 | } 55 | } 56 | 57 | @POST public LinkToken getLinkToken() throws IOException { 58 | 59 | 60 | String clientUserId = Long.toString((new Date()).getTime()); 61 | LinkTokenCreateRequestUser user = new LinkTokenCreateRequestUser() 62 | .clientUserId(clientUserId); 63 | 64 | for (int i = 0; i < this.plaidProducts.size(); i++){ 65 | this.correctedPlaidProducts.add(Products.fromValue(this.plaidProducts.get(i))); 66 | }; 67 | 68 | for (int i = 0; i < this.countryCodes.size(); i++){ 69 | this.correctedCountryCodes.add(CountryCode.fromValue(this.countryCodes.get(i))); 70 | }; 71 | 72 | 73 | LinkTokenCreateRequest request = new LinkTokenCreateRequest() 74 | .user(user) 75 | .clientName("Quickstart Client") 76 | .products(this.correctedPlaidProducts) 77 | .countryCodes(this.correctedCountryCodes) 78 | .language("en") 79 | .redirectUri(this.redirectUri); 80 | 81 | if (this.correctedPlaidProducts.contains(Products.STATEMENTS)) { 82 | LinkTokenCreateRequestStatements statementsConfig = new LinkTokenCreateRequestStatements() 83 | .startDate(LocalDate.now().minusDays(30)) 84 | .endDate(LocalDate.now()); 85 | request.setStatements(statementsConfig); 86 | } 87 | 88 | if (plaidProducts.stream().anyMatch(product -> product.startsWith("cra_"))) { 89 | request.userToken(QuickstartApplication.userToken); 90 | request.consumerReportPermissiblePurpose(ConsumerReportPermissiblePurpose.ACCOUNT_REVIEW_CREDIT); 91 | LinkTokenCreateRequestCraOptions options = new LinkTokenCreateRequestCraOptions(); 92 | options.daysRequested(60); 93 | request.craOptions(options); 94 | } 95 | 96 | Response response =plaidClient 97 | .linkTokenCreate(request) 98 | .execute(); 99 | return new LinkToken(response.body().getLinkToken()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/LinkTokenWithPaymentResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.plaid.client.request.PlaidApi; 6 | import com.plaid.client.model.PaymentAmount; 7 | import com.plaid.client.model.PaymentAmountCurrency; 8 | import com.plaid.client.model.Products; 9 | import com.plaid.client.model.CountryCode; 10 | import com.plaid.client.model.PaymentInitiationPaymentCreateRequest; 11 | import com.plaid.client.model.PaymentInitiationPaymentCreateResponse; 12 | import com.plaid.client.model.LinkTokenCreateRequest; 13 | import com.plaid.client.model.LinkTokenCreateRequestUser; 14 | import com.plaid.client.model.LinkTokenCreateResponse; 15 | import com.plaid.client.model.PaymentInitiationRecipientCreateResponse; 16 | import com.plaid.client.model.PaymentInitiationAddress; 17 | import com.plaid.client.model.PaymentInitiationRecipientCreateRequest; 18 | import com.plaid.client.model.LinkTokenCreateRequestPaymentInitiation; 19 | import com.plaid.quickstart.QuickstartApplication; 20 | 21 | import java.util.Arrays; 22 | import java.util.List; 23 | import java.util.Date; 24 | 25 | import javax.ws.rs.POST; 26 | import javax.ws.rs.Path; 27 | import javax.ws.rs.Produces; 28 | import javax.ws.rs.core.MediaType; 29 | 30 | import retrofit2.Response; 31 | 32 | @Path("/create_link_token_for_payment") 33 | @Produces(MediaType.APPLICATION_JSON) 34 | public class LinkTokenWithPaymentResource { 35 | private final PlaidApi plaidClient; 36 | private final String redirectUri; 37 | 38 | public LinkTokenWithPaymentResource(PlaidApi plaidClient, List plaidProducts, 39 | List countryCodes, String redirectUri) { 40 | this.plaidClient = plaidClient; 41 | this.redirectUri = redirectUri; 42 | } 43 | 44 | @POST public LinkTokenResource.LinkToken getLinkToken() throws IOException { 45 | 46 | PaymentInitiationAddress address = new PaymentInitiationAddress() 47 | .street(Arrays.asList("Street Name 999")) 48 | .city("City") 49 | .postalCode("99999") 50 | .country("GB"); 51 | 52 | PaymentInitiationRecipientCreateRequest recipientCreateRequest = new PaymentInitiationRecipientCreateRequest() 53 | .name("Jonathan Doe") 54 | .iban("GB33BUKB20201555555555") 55 | .address(address); 56 | 57 | 58 | Response recipientResponse = 59 | this.plaidClient 60 | .paymentInitiationRecipientCreate(recipientCreateRequest) 61 | .execute(); 62 | 63 | 64 | String recipientId = recipientResponse.body().getRecipientId(); 65 | 66 | PaymentAmount amount = new PaymentAmount() 67 | .currency(PaymentAmountCurrency.GBP) 68 | .value(999.99); 69 | 70 | PaymentInitiationPaymentCreateRequest paymentCreateRequest = new PaymentInitiationPaymentCreateRequest() 71 | .recipientId(recipientId) 72 | .reference("reference") 73 | .amount(amount); 74 | 75 | Response paymentResponse = plaidClient 76 | .paymentInitiationPaymentCreate(paymentCreateRequest) 77 | .execute(); 78 | 79 | 80 | String paymentId = paymentResponse.body().getPaymentId(); 81 | QuickstartApplication.paymentId = paymentId; 82 | 83 | LinkTokenCreateRequestPaymentInitiation paymentInitiation = new LinkTokenCreateRequestPaymentInitiation() 84 | .paymentId(paymentId); 85 | 86 | // This should correspond to a unique id for the current user. 87 | // Typically, this will be a user ID number from your application. 88 | // Personally identifiable information, such as an email address or phone number, should not be used here. 89 | String clientUserId = Long.toString((new Date()).getTime()); 90 | LinkTokenCreateRequestUser user = new LinkTokenCreateRequestUser() 91 | .clientUserId(clientUserId); 92 | 93 | LinkTokenCreateRequest request = new LinkTokenCreateRequest() 94 | .user(user) 95 | .clientName("Quickstart Client") 96 | // The 'payment_initiation' product has to be the only element in the 'products' list. 97 | .products(Arrays.asList(Products.PAYMENT_INITIATION)) 98 | // Institutions from all listed countries will be shown. 99 | .countryCodes(Arrays.asList(CountryCode.GB)) 100 | .language("en") 101 | .redirectUri(this.redirectUri) 102 | .paymentInitiation(paymentInitiation); 103 | 104 | Response response =plaidClient 105 | .linkTokenCreate(request) 106 | .execute(); 107 | 108 | return new LinkTokenResource.LinkToken(response.body().getLinkToken()); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/PaymentInitiationResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.google.gson.Gson; 7 | 8 | import com.plaid.client.request.PlaidApi; 9 | import com.plaid.client.model.PaymentInitiationPaymentGetRequest; 10 | import com.plaid.client.model.PaymentInitiationPaymentGetResponse; 11 | import com.plaid.quickstart.QuickstartApplication; 12 | 13 | import javax.ws.rs.GET; 14 | import javax.ws.rs.Path; 15 | import javax.ws.rs.Produces; 16 | import javax.ws.rs.core.MediaType; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import retrofit2.Response; 22 | 23 | // This functionality is only relevant for the UK Payment Initiation product. 24 | @Path("/payment") 25 | @Produces(MediaType.APPLICATION_JSON) 26 | public class PaymentInitiationResource { 27 | private static final Logger LOG = LoggerFactory.getLogger(PaymentInitiationResource.class); 28 | 29 | private final PlaidApi plaidClient; 30 | 31 | public PaymentInitiationResource(PlaidApi plaidClient) { 32 | this.plaidClient = plaidClient; 33 | } 34 | 35 | @GET 36 | public PaymentResponse getPayment() throws IOException { 37 | String paymentId = QuickstartApplication.paymentId; 38 | 39 | PaymentInitiationPaymentGetRequest request = new PaymentInitiationPaymentGetRequest() 40 | .paymentId(paymentId); 41 | 42 | Response response = 43 | plaidClient 44 | .paymentInitiationPaymentGet(request) 45 | .execute(); 46 | if (!response.isSuccessful()) { 47 | try { 48 | Gson gson = new Gson(); 49 | Error errorResponse = gson.fromJson(response.errorBody().string(), Error.class); 50 | LOG.error("error: " + errorResponse); 51 | } catch (Exception e) { 52 | LOG.error("error", e); 53 | } 54 | } 55 | return new PaymentResponse(response.body()); 56 | } 57 | 58 | private static class PaymentResponse { 59 | @JsonProperty 60 | private final PaymentInitiationPaymentGetResponse payment; 61 | 62 | public PaymentResponse(PaymentInitiationPaymentGetResponse response) { 63 | this.payment = response; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/PublicTokenResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | 6 | import com.plaid.client.request.PlaidApi; 7 | import com.plaid.client.model.ItemPublicTokenCreateRequest; 8 | import com.plaid.client.model.ItemPublicTokenCreateResponse; 9 | import com.plaid.quickstart.QuickstartApplication; 10 | 11 | import javax.ws.rs.GET; 12 | import javax.ws.rs.Path; 13 | import javax.ws.rs.Produces; 14 | import javax.ws.rs.core.MediaType; 15 | 16 | import retrofit2.Response; 17 | 18 | @Path("/create_public_token") 19 | @Produces(MediaType.APPLICATION_JSON) 20 | public class PublicTokenResource { 21 | private final PlaidApi plaidClient; 22 | 23 | public PublicTokenResource(PlaidApi plaidClient) { 24 | this.plaidClient = plaidClient; 25 | } 26 | 27 | @GET 28 | public ItemPublicTokenCreateResponse createPublicToken() throws IOException { 29 | 30 | ItemPublicTokenCreateRequest request = new ItemPublicTokenCreateRequest() 31 | .accessToken(QuickstartApplication.accessToken); 32 | 33 | Response response = plaidClient 34 | .itemCreatePublicToken(request) 35 | .execute(); 36 | 37 | return response.body(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/SignalResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.plaid.client.request.PlaidApi; 7 | import com.plaid.client.model.AccountsGetRequest; 8 | import com.plaid.client.model.AccountsGetResponse; 9 | import com.plaid.client.model.AccountIdentity; 10 | import com.plaid.client.model.SignalEvaluateRequest; 11 | import com.plaid.client.model.SignalEvaluateResponse; 12 | import com.plaid.quickstart.QuickstartApplication; 13 | 14 | import java.util.List; 15 | import javax.ws.rs.GET; 16 | import javax.ws.rs.Path; 17 | import javax.ws.rs.Produces; 18 | import javax.ws.rs.core.MediaType; 19 | 20 | import retrofit2.Response; 21 | 22 | @Path("/signal_evaluate") 23 | @Produces(MediaType.APPLICATION_JSON) 24 | public class SignalResource { 25 | private final PlaidApi plaidClient; 26 | 27 | public SignalResource(PlaidApi plaidClient) { 28 | this.plaidClient = plaidClient; 29 | } 30 | 31 | @GET 32 | public SignalEvaluateResponse signalEvaluate() throws IOException { 33 | AccountsGetRequest accountsGetRequest = new AccountsGetRequest() 34 | .accessToken(QuickstartApplication.accessToken); 35 | 36 | Response accountsGetResponse = plaidClient 37 | .accountsGet(accountsGetRequest) 38 | .execute(); 39 | 40 | QuickstartApplication.accountId = accountsGetResponse.body().getAccounts().get(0).getAccountId(); 41 | 42 | SignalEvaluateRequest signalEvaluateRequest = new SignalEvaluateRequest() 43 | .accessToken(QuickstartApplication.accessToken) 44 | .accountId(QuickstartApplication.accountId) 45 | .clientTransactionId("txn1234") 46 | .amount(100.00); 47 | 48 | Response signalEvaluateResponse = plaidClient 49 | .signalEvaluate(signalEvaluateRequest) 50 | .execute(); 51 | 52 | return signalEvaluateResponse.body(); 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/StatementsResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.plaid.client.request.PlaidApi; 7 | import com.plaid.client.model.StatementsListRequest; 8 | import com.plaid.client.model.StatementsListResponse; 9 | import com.plaid.client.model.StatementsDownloadRequest; 10 | import com.plaid.quickstart.QuickstartApplication; 11 | import okhttp3.ResponseBody; 12 | 13 | import java.util.List; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.Base64; 17 | import javax.ws.rs.GET; 18 | import javax.ws.rs.Path; 19 | import javax.ws.rs.Produces; 20 | import javax.ws.rs.core.MediaType; 21 | 22 | import retrofit2.Response; 23 | 24 | @Path("/statements") 25 | @Produces(MediaType.APPLICATION_JSON) 26 | public class StatementsResource { 27 | private final PlaidApi plaidClient; 28 | 29 | public StatementsResource(PlaidApi plaidClient) { 30 | this.plaidClient = plaidClient; 31 | } 32 | 33 | @GET 34 | public Map statementsList() throws IOException { 35 | 36 | StatementsListRequest statementsListRequest = new StatementsListRequest() 37 | .accessToken(QuickstartApplication.accessToken); 38 | 39 | Response statementsListResponse = plaidClient 40 | .statementsList(statementsListRequest) 41 | .execute(); 42 | 43 | StatementsDownloadRequest statementsDownloadRequest = new StatementsDownloadRequest() 44 | .accessToken(QuickstartApplication.accessToken) 45 | .statementId(statementsListResponse.body().getAccounts().get(0).getStatements().get(0).getStatementId()); 46 | 47 | Response statementsDownloadResponse = plaidClient 48 | .statementsDownload(statementsDownloadRequest) 49 | .execute(); 50 | 51 | String pdf = Base64.getEncoder().encodeToString(statementsDownloadResponse.body().bytes()); 52 | 53 | Map responseMap = new HashMap<>(); 54 | responseMap.put("json", statementsListResponse.body()); 55 | responseMap.put("pdf", pdf); 56 | 57 | return responseMap; 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/TransactionsResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.Comparator; 6 | import java.util.List; 7 | 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | 10 | import com.plaid.client.request.PlaidApi; 11 | import com.plaid.client.model.TransactionsSyncRequest; 12 | import com.plaid.client.model.TransactionsSyncResponse; 13 | import com.plaid.client.model.Transaction; 14 | import com.plaid.client.model.RemovedTransaction; 15 | import com.plaid.quickstart.QuickstartApplication; 16 | 17 | import javax.ws.rs.GET; 18 | import javax.ws.rs.Path; 19 | import javax.ws.rs.Produces; 20 | import javax.ws.rs.core.MediaType; 21 | 22 | import retrofit2.Response; 23 | 24 | @Path("/transactions") 25 | @Produces(MediaType.APPLICATION_JSON) 26 | public class TransactionsResource { 27 | private final PlaidApi plaidClient; 28 | 29 | 30 | public TransactionsResource(PlaidApi plaidClient) { 31 | this.plaidClient = plaidClient; 32 | } 33 | 34 | @GET 35 | public TransactionsResponse getTransactions() throws IOException, InterruptedException { 36 | // Set cursor to empty to receive all historical updates 37 | String cursor = null; 38 | 39 | // New transaction updates since "cursor" 40 | List added = new ArrayList(); 41 | List modified = new ArrayList(); 42 | List removed = new ArrayList(); 43 | boolean hasMore = true; 44 | // Iterate through each page of new transaction updates for item 45 | while (hasMore) { 46 | TransactionsSyncRequest request = new TransactionsSyncRequest() 47 | .accessToken(QuickstartApplication.accessToken) 48 | .cursor(cursor); 49 | 50 | Response response = plaidClient.transactionsSync(request).execute(); 51 | TransactionsSyncResponse responseBody = response.body(); 52 | 53 | cursor = responseBody.getNextCursor(); 54 | 55 | // If no transactions are available yet, wait and poll the endpoint. 56 | // Normally, we would listen for a webhook, but the Quickstart doesn't 57 | // support webhooks. For a webhook example, see 58 | // https://github.com/plaid/tutorial-resources or 59 | // https://github.com/plaid/pattern 60 | 61 | if (cursor.equals("")) { 62 | Thread.sleep(2000); 63 | continue; 64 | } 65 | // Add this page of results 66 | added.addAll(responseBody.getAdded()); 67 | modified.addAll(responseBody.getModified()); 68 | removed.addAll(responseBody.getRemoved()); 69 | hasMore = responseBody.getHasMore(); 70 | } 71 | 72 | // Return the 8 most recent transactions 73 | added.sort(new TransactionsResource.CompareTransactionDate()); 74 | List latestTransactions = added.subList(Math.max(added.size() - 8, 0), added.size()); 75 | return new TransactionsResponse(latestTransactions); 76 | } 77 | 78 | private class CompareTransactionDate implements Comparator { 79 | @Override 80 | public int compare(Transaction o1, Transaction o2) { 81 | return o1.getDate().compareTo(o2.getDate()); 82 | } 83 | } 84 | 85 | private static class TransactionsResponse { 86 | @JsonProperty 87 | private final List latest_transactions; 88 | 89 | public TransactionsResponse(List latestTransactions) { 90 | this.latest_transactions = latestTransactions; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/TransferAuthorizeResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.plaid.client.request.PlaidApi; 7 | import com.plaid.client.model.Transfer; 8 | import com.plaid.quickstart.QuickstartApplication; 9 | import com.plaid.client.model.TransferAuthorizationUserInRequest; 10 | import com.plaid.client.model.TransferAuthorizationCreateRequest; 11 | import com.plaid.client.model.TransferAuthorizationCreateResponse; 12 | import com.plaid.client.model.TransferType; 13 | import com.plaid.client.model.TransferNetwork; 14 | import com.plaid.client.model.ACHClass; 15 | import com.plaid.client.model.AccountsGetRequest; 16 | import com.plaid.client.model.AccountsGetResponse; 17 | 18 | import java.util.List; 19 | import javax.ws.rs.GET; 20 | import javax.ws.rs.Path; 21 | import javax.ws.rs.Produces; 22 | import javax.ws.rs.core.MediaType; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import retrofit2.Response; 28 | 29 | @Path("/transfer_authorize") 30 | @Produces(MediaType.APPLICATION_JSON) 31 | public class TransferAuthorizeResource { 32 | private final PlaidApi plaidClient; 33 | 34 | public TransferAuthorizeResource(PlaidApi plaidClient) { 35 | this.plaidClient = plaidClient; 36 | } 37 | 38 | @GET 39 | public TransferAuthorizationCreateResponse authorizeTransfer() throws IOException { 40 | AccountsGetRequest accountsGetRequest = new AccountsGetRequest() 41 | .accessToken(QuickstartApplication.accessToken); 42 | 43 | Response accountsGetResponse = plaidClient 44 | .accountsGet(accountsGetRequest) 45 | .execute(); 46 | 47 | QuickstartApplication.accountId = accountsGetResponse.body().getAccounts().get(0).getAccountId(); 48 | 49 | TransferAuthorizationUserInRequest user = new TransferAuthorizationUserInRequest() 50 | .legalName("FirstName LastName"); 51 | 52 | TransferAuthorizationCreateRequest transferAuthorizationCreateRequest = new TransferAuthorizationCreateRequest() 53 | .accessToken(QuickstartApplication.accessToken) 54 | .accountId(QuickstartApplication.accountId) 55 | .type(TransferType.DEBIT) 56 | .network(TransferNetwork.ACH) 57 | .amount("1.00") 58 | .achClass(ACHClass.PPD) 59 | .user(user); 60 | 61 | Response transferAuthorizationCreateResponse = plaidClient 62 | .transferAuthorizationCreate(transferAuthorizationCreateRequest) 63 | .execute(); 64 | 65 | QuickstartApplication.authorizationId = transferAuthorizationCreateResponse.body().getAuthorization().getId(); 66 | return transferAuthorizationCreateResponse.body(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/TransferCreateResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.plaid.client.request.PlaidApi; 7 | import com.plaid.client.model.Transfer; 8 | import com.plaid.quickstart.QuickstartApplication; 9 | import com.plaid.client.model.TransferCreateRequest; 10 | import com.plaid.client.model.TransferCreateResponse; 11 | 12 | import java.util.List; 13 | import javax.ws.rs.GET; 14 | import javax.ws.rs.Path; 15 | import javax.ws.rs.Produces; 16 | import javax.ws.rs.core.MediaType; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import retrofit2.Response; 22 | 23 | @Path("/transfer_create") 24 | @Produces(MediaType.APPLICATION_JSON) 25 | public class TransferCreateResource { 26 | private final PlaidApi plaidClient; 27 | 28 | public TransferCreateResource(PlaidApi plaidClient) { 29 | this.plaidClient = plaidClient; 30 | } 31 | 32 | @GET 33 | public TransferCreateResponse createTransfer() throws IOException { 34 | TransferCreateRequest request = new TransferCreateRequest() 35 | .authorizationId(QuickstartApplication.authorizationId) 36 | .accessToken(QuickstartApplication.accessToken) 37 | .accountId(QuickstartApplication.accountId) 38 | .description("Debit"); 39 | Response transferCreateResponse = plaidClient 40 | .transferCreate(request) 41 | .execute(); 42 | return transferCreateResponse.body(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /java/src/main/java/com/plaid/quickstart/resources/UserTokenResource.java: -------------------------------------------------------------------------------- 1 | package com.plaid.quickstart.resources; 2 | 3 | import com.plaid.client.model.AddressData; 4 | import com.plaid.client.model.ConsumerReportUserIdentity; 5 | import com.plaid.client.model.UserCreateRequest; 6 | import com.plaid.client.model.UserCreateResponse; 7 | import com.plaid.client.request.PlaidApi; 8 | import com.plaid.quickstart.QuickstartApplication; 9 | import retrofit2.Response; 10 | 11 | import javax.ws.rs.POST; 12 | import javax.ws.rs.Path; 13 | import javax.ws.rs.Produces; 14 | import javax.ws.rs.core.MediaType; 15 | import java.io.IOException; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.UUID; 19 | 20 | @Path("/create_user_token") 21 | @Produces(MediaType.APPLICATION_JSON) 22 | public class UserTokenResource { 23 | private final PlaidApi plaidClient; 24 | private final List plaidProducts; 25 | 26 | public UserTokenResource(PlaidApi plaidClient, List plaidProducts) { 27 | this.plaidClient = plaidClient; 28 | this.plaidProducts = plaidProducts; 29 | } 30 | 31 | // Create a user token which can be used for Plaid Check, Income, or Multi-Item link flows 32 | // https://plaid.com/docs/api/users/#usercreate 33 | @POST 34 | public UserCreateResponse createUserToken() throws IOException { 35 | 36 | UserCreateRequest userCreateRequest = new UserCreateRequest() 37 | // Typically, this will be a user ID number from your application. 38 | .clientUserId("user_" + UUID.randomUUID()); 39 | 40 | if (plaidProducts.stream().anyMatch(product -> product.startsWith("cra_"))) { 41 | AddressData addressData = new AddressData() 42 | .city("New York") 43 | .region("NY") 44 | .street("4 Priver Drive") 45 | .postalCode("11111") 46 | .country("US"); 47 | userCreateRequest.consumerReportUserIdentity(new ConsumerReportUserIdentity() 48 | .firstName("Harry") 49 | .lastName("Potter") 50 | .phoneNumbers(Arrays.asList("+16174567890")) 51 | .emails(List.of("harrypotter@example.com")) 52 | .primaryAddress(addressData)); 53 | } 54 | Response userResponse = plaidClient.userCreate(userCreateRequest).execute(); 55 | 56 | // Ideally, we would store this somewhere more persistent 57 | QuickstartApplication.userToken = userResponse.body().getUserToken(); 58 | return userResponse.body(); 59 | } 60 | } -------------------------------------------------------------------------------- /java/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use the env variables in .env to start the server 3 | env $(cat .env | grep -v "#" | xargs) java -jar target/quickstart-1.0-SNAPSHOT.jar server config.yml 4 | -------------------------------------------------------------------------------- /node/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /node/.env: -------------------------------------------------------------------------------- 1 | ../.env -------------------------------------------------------------------------------- /node/.env.example: -------------------------------------------------------------------------------- 1 | ../.env.example -------------------------------------------------------------------------------- /node/.gitignore: -------------------------------------------------------------------------------- 1 | .prettierrc -------------------------------------------------------------------------------- /node/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.com 2 | -------------------------------------------------------------------------------- /node/.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.1 2 | -------------------------------------------------------------------------------- /node/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "requirePragma": false, 3 | "jsxSingleQuote": true, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "semi": true 9 | } 10 | -------------------------------------------------------------------------------- /node/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /opt/app 4 | RUN chown -R node:node /opt/app 5 | COPY --chown=node:node ./node/package*.json /opt/app/ 6 | 7 | USER node 8 | RUN npm install 9 | 10 | COPY --chown=node:node ./node/index.js ./ 11 | COPY --chown=node:node ./.env ./ 12 | 13 | EXPOSE 8000 14 | ENTRYPOINT ["node"] 15 | CMD ["index.js"] 16 | -------------------------------------------------------------------------------- /node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plaid-node-walkthrough", 3 | "version": "0.1.0", 4 | "description": "A sample app and accompanying walkthrough for the Plaid API.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "watch": "nodemon index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "developers@plaid.com", 12 | "repository": "https://github.com/plaid/quickstart", 13 | "license": "ISC", 14 | "dependencies": { 15 | "body-parser": "^1.20.3", 16 | "cors": "^2.8.5", 17 | "dotenv": "^8.2.0", 18 | "ejs": "^3.1.10", 19 | "express": "^4.21.1", 20 | "moment": "^2.30.1", 21 | "nodemon": "^3.1.7", 22 | "plaid": "^30.0.0", 23 | "uuid": "^9.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /node/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | node index.js -------------------------------------------------------------------------------- /python/.env: -------------------------------------------------------------------------------- 1 | ../.env -------------------------------------------------------------------------------- /python/.env.example: -------------------------------------------------------------------------------- 1 | ../.env.example -------------------------------------------------------------------------------- /python/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | 3 | WORKDIR /opt/app 4 | COPY . . 5 | WORKDIR /opt/app/python 6 | 7 | RUN pip3 install -r requirements.txt 8 | 9 | ENV FLASK_APP=/opt/app/python/server.py 10 | EXPOSE 8000 11 | ENTRYPOINT ["python"] 12 | CMD ["-m", "flask", "run", "--host=0.0.0.0", "--port=8000"] 13 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.0 2 | plaid_python==28.0.0 3 | python-dotenv==0.15.0 4 | itsdangerous==2.1.2 5 | werkzeug==3.1.3 6 | -------------------------------------------------------------------------------- /python/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | python server.py || python3 server.py -------------------------------------------------------------------------------- /ruby/.env: -------------------------------------------------------------------------------- 1 | ../.env -------------------------------------------------------------------------------- /ruby/.env.example: -------------------------------------------------------------------------------- 1 | ../.env.example -------------------------------------------------------------------------------- /ruby/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | .bundle 3 | -------------------------------------------------------------------------------- /ruby/.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 2 | -------------------------------------------------------------------------------- /ruby/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.0 2 | 3 | WORKDIR /opt/app 4 | COPY . . 5 | WORKDIR /opt/app/ruby 6 | 7 | RUN bundle update --bundler 8 | RUN bundle install 9 | 10 | EXPOSE 8000 11 | ENTRYPOINT ["ruby"] 12 | CMD ["app.rb", "-o", "0.0.0.0"] 13 | -------------------------------------------------------------------------------- /ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sinatra' 4 | 5 | gem 'http' 6 | 7 | gem "dotenv", "~> 2.7" 8 | 9 | gem "plaid", "= 34.0.0" 10 | 11 | gem "webrick" 12 | 13 | gem "rackup", "~> 2.2" 14 | gem "puma", "~> 6.5" 15 | -------------------------------------------------------------------------------- /ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.7) 5 | public_suffix (>= 2.0.2, < 7.0) 6 | base64 (0.2.0) 7 | domain_name (0.6.20240107) 8 | dotenv (2.8.1) 9 | faraday (2.12.1) 10 | faraday-net_http (>= 2.0, < 3.5) 11 | json 12 | logger 13 | faraday-multipart (1.0.4) 14 | multipart-post (~> 2) 15 | faraday-net_http (3.4.0) 16 | net-http (>= 0.5.0) 17 | ffi (1.17.0) 18 | ffi-compiler (1.3.2) 19 | ffi (>= 1.15.5) 20 | rake 21 | http (5.2.0) 22 | addressable (~> 2.8) 23 | base64 (~> 0.1) 24 | http-cookie (~> 1.0) 25 | http-form_data (~> 2.2) 26 | llhttp-ffi (~> 0.5.0) 27 | http-cookie (1.0.7) 28 | domain_name (~> 0.5) 29 | http-form_data (2.3.0) 30 | json (2.8.2) 31 | llhttp-ffi (0.5.0) 32 | ffi-compiler (~> 1.0) 33 | rake (~> 13.0) 34 | logger (1.6.1) 35 | multipart-post (2.4.1) 36 | mustermann (3.0.3) 37 | ruby2_keywords (~> 0.0.1) 38 | net-http (0.5.0) 39 | uri 40 | nio4r (2.7.4) 41 | plaid (34.0.0) 42 | faraday (>= 1.0.1, < 3.0) 43 | faraday-multipart (>= 1.0.1, < 2.0) 44 | public_suffix (6.0.1) 45 | puma (6.5.0) 46 | nio4r (~> 2.0) 47 | rack (3.1.8) 48 | rack-protection (4.1.1) 49 | base64 (>= 0.1.0) 50 | logger (>= 1.6.0) 51 | rack (>= 3.0.0, < 4) 52 | rack-session (2.0.0) 53 | rack (>= 3.0.0) 54 | rackup (2.2.1) 55 | rack (>= 3) 56 | rake (13.2.1) 57 | ruby2_keywords (0.0.5) 58 | sinatra (4.1.1) 59 | logger (>= 1.6.0) 60 | mustermann (~> 3.0) 61 | rack (>= 3.0.0, < 4) 62 | rack-protection (= 4.1.1) 63 | rack-session (>= 2.0.0, < 3) 64 | tilt (~> 2.0) 65 | tilt (2.4.0) 66 | uri (1.0.2) 67 | webrick (1.9.0) 68 | 69 | PLATFORMS 70 | ruby 71 | 72 | DEPENDENCIES 73 | dotenv (~> 2.7) 74 | http 75 | plaid (= 34.0.0) 76 | puma (~> 6.5) 77 | rackup (~> 2.2) 78 | sinatra 79 | webrick 80 | 81 | BUNDLED WITH 82 | 2.4.10 83 | -------------------------------------------------------------------------------- /ruby/app.rb: -------------------------------------------------------------------------------- 1 | # Load env vars from .env file 2 | require 'dotenv' 3 | Dotenv.load 4 | 5 | require 'base64' 6 | require 'date' 7 | require 'json' 8 | require 'plaid' 9 | require 'sinatra' 10 | 11 | set :port, ENV['APP_PORT'] || 8000 12 | 13 | # disable CSRF warning and Rack protection on localhost due to usage of local /api proxy in react app. 14 | # delete this for a production application. 15 | set :protection, :except => [:json_csrf] 16 | set :host_authorization, { permitted_hosts: [] } 17 | 18 | configuration = Plaid::Configuration.new 19 | configuration.server_index = Plaid::Configuration::Environment[ENV['PLAID_ENV'] || 'sandbox'] 20 | configuration.api_key['PLAID-CLIENT-ID'] = ENV['PLAID_CLIENT_ID'] 21 | configuration.api_key['PLAID-SECRET'] = ENV['PLAID_SECRET'] 22 | configuration.api_key['Plaid-Version'] = '2020-09-14' 23 | 24 | api_client = Plaid::ApiClient.new( 25 | configuration 26 | ) 27 | 28 | client = Plaid::PlaidApi.new(api_client) 29 | products = ENV['PLAID_PRODUCTS'].split(',') 30 | # We store the access_token and user_token in memory - in production, store it in a secure 31 | # persistent data store. 32 | access_token = nil 33 | user_token = nil 34 | # The payment_id is only relevant for the UK Payment Initiation product. 35 | # We store the payment_token in memory - in production, store it in a secure 36 | # persistent data store. 37 | 38 | payment_id = nil 39 | item_id = nil 40 | 41 | # The authorization_id is only relevant for Transfer ACH product. 42 | # We store the authorization_id in memory - in production, store it in a secure 43 | # persistent data store. 44 | authorization_id = nil 45 | account_id = nil 46 | 47 | post '/api/info' do 48 | content_type :json 49 | { 50 | item_id: item_id, 51 | access_token: access_token, 52 | products: ENV['PLAID_PRODUCTS'].split(','), 53 | }.to_json 54 | end 55 | 56 | # Exchange token flow - exchange a Link public_token for 57 | # an API access_token 58 | # https://plaid.com/docs/#exchange-token-flow 59 | post '/api/set_access_token' do 60 | item_public_token_exchange_request = Plaid::ItemPublicTokenExchangeRequest.new( 61 | { public_token: params['public_token'] } 62 | ) 63 | exchange_token_response = 64 | client.item_public_token_exchange( 65 | item_public_token_exchange_request 66 | ) 67 | access_token = exchange_token_response.access_token 68 | item_id = exchange_token_response.item_id 69 | pretty_print_response(exchange_token_response.to_hash) 70 | content_type :json 71 | exchange_token_response.to_hash.to_json 72 | end 73 | 74 | # Retrieve Transactions for an Item 75 | # https://plaid.com/docs/#transactions 76 | get '/api/transactions' do 77 | begin 78 | # Set cursor to empty to receive all historical updates 79 | cursor = '' 80 | 81 | # New transaction updates since "cursor" 82 | added = [] 83 | modified = [] 84 | removed = [] # Removed transaction ids 85 | has_more = true 86 | # Iterate through each page of new transaction updates for item 87 | while has_more 88 | request = Plaid::TransactionsSyncRequest.new( 89 | { 90 | access_token: access_token, 91 | cursor: cursor 92 | } 93 | ) 94 | response = client.transactions_sync(request) 95 | cursor = response.next_cursor 96 | 97 | # If no transactions are available yet, wait and poll the endpoint. 98 | # Normally, we would listen for a webhook but the Quickstart doesn't 99 | # support webhooks. For a webhook example, see 100 | # https://github.com/plaid/tutorial-resources or 101 | # https://github.com/plaid/pattern 102 | if cursor == "" 103 | sleep 2 104 | next 105 | end 106 | 107 | # Add this page of results 108 | added += response.added 109 | modified += response.modified 110 | removed += response.removed 111 | has_more = response.has_more 112 | pretty_print_response(response.to_hash) 113 | end 114 | # Return the 8 most recent transactions 115 | content_type :json 116 | { latest_transactions: added.sort_by(&:date).last(8).map(&:to_hash) }.to_json 117 | rescue Plaid::ApiError => e 118 | error_response = format_error(e) 119 | pretty_print_response(error_response) 120 | content_type :json 121 | error_response.to_json 122 | end 123 | end 124 | 125 | # Retrieve ACH or ETF account numbers for an Item 126 | # https://plaid.com/docs/#auth 127 | get '/api/auth' do 128 | begin 129 | auth_get_request = Plaid::AuthGetRequest.new({ access_token: access_token }) 130 | auth_response = client.auth_get(auth_get_request) 131 | pretty_print_response(auth_response.to_hash) 132 | content_type :json 133 | auth_response.to_hash.to_json 134 | rescue Plaid::ApiError => e 135 | error_response = format_error(e) 136 | pretty_print_response(error_response) 137 | content_type :json 138 | error_response.to_json 139 | end 140 | end 141 | 142 | # Retrieve Identity data for an Item 143 | # https://plaid.com/docs/#identity 144 | get '/api/identity' do 145 | begin 146 | identity_get_request = Plaid::IdentityGetRequest.new({ access_token: access_token }) 147 | identity_response = client.identity_get(identity_get_request) 148 | pretty_print_response(identity_response.to_hash) 149 | content_type :json 150 | { identity: identity_response.to_hash[:accounts] }.to_json 151 | rescue Plaid::ApiError => e 152 | error_response = format_error(e) 153 | pretty_print_response(error_response) 154 | content_type :json 155 | error_response.to_json 156 | end 157 | end 158 | 159 | # Retrieve real-time balance data for each of an Item's accounts 160 | # https://plaid.com/docs/#balance 161 | get '/api/balance' do 162 | begin 163 | balance_get_request = Plaid::AccountsBalanceGetRequest.new({ access_token: access_token }) 164 | balance_response = client.accounts_balance_get(balance_get_request) 165 | pretty_print_response(balance_response.to_hash) 166 | content_type :json 167 | balance_response.to_hash.to_json 168 | rescue Plaid::ApiError => e 169 | error_response = format_error(e) 170 | pretty_print_response(error_response) 171 | content_type :json 172 | error_response.to_json 173 | end 174 | end 175 | 176 | # Retrieve an Item's accounts 177 | # https://plaid.com/docs/#accounts 178 | get '/api/accounts' do 179 | begin 180 | accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token }) 181 | account_response = client.accounts_get(accounts_get_request) 182 | pretty_print_response(account_response.to_hash) 183 | content_type :json 184 | account_response.to_hash.to_json 185 | rescue Plaid::ApiError => e 186 | error_response = format_error(e) 187 | pretty_print_response(error_response) 188 | content_type :json 189 | error_response.to_json 190 | end 191 | end 192 | 193 | # Retrieve Holdings data for an Item 194 | # https://plaid.com/docs/#investments 195 | get '/api/holdings' do 196 | begin 197 | investments_holdings_get_request = Plaid::InvestmentsHoldingsGetRequest.new({ access_token: access_token }) 198 | product_response = client.investments_holdings_get(investments_holdings_get_request) 199 | pretty_print_response(product_response.to_hash) 200 | content_type :json 201 | { holdings: product_response.to_hash }.to_json 202 | rescue Plaid::ApiError => e 203 | error_response = format_error(e) 204 | pretty_print_response(error_response) 205 | content_type :json 206 | error_response.to_hash.to_json 207 | end 208 | end 209 | 210 | # Retrieve Investment Transactions for an Item 211 | # https://plaid.com/docs/#investments 212 | get '/api/investments_transactions' do 213 | begin 214 | start_date = (Date.today - 30) 215 | end_date = Date.today 216 | investments_transactions_get_request = Plaid::InvestmentsTransactionsGetRequest.new( 217 | { 218 | access_token: access_token, 219 | start_date: start_date, 220 | end_date: end_date 221 | } 222 | ) 223 | transactions_response = client.investments_transactions_get(investments_transactions_get_request) 224 | pretty_print_response(transactions_response.to_hash) 225 | content_type :json 226 | { investments_transactions: transactions_response.to_hash }.to_json 227 | rescue Plaid::ApiError => e 228 | error_response = format_error(e) 229 | pretty_print_response(error_response) 230 | content_type :json 231 | error_response.to_json 232 | end 233 | end 234 | 235 | # Create and then retrieve an Asset Report for one or more Items. Note that an 236 | # Asset Report can contain up to 100 items, but for simplicity we're only 237 | # including one Item here. 238 | # https://plaid.com/docs/#assets 239 | # rubocop:disable Metrics/BlockLength 240 | get '/api/assets' do 241 | begin 242 | options = { 243 | client_report_id: '123', 244 | webhook: 'https://www.example.com', 245 | user: { 246 | client_user_id: '789', 247 | first_name: 'Jane', 248 | middle_name: 'Leah', 249 | last_name: 'Doe', 250 | ssn: '123-45-6789', 251 | phone_number: '(555) 123-4567', 252 | email: 'jane.doe@example.com' 253 | } 254 | } 255 | asset_report_create_request = Plaid::AssetReportCreateRequest.new( 256 | { 257 | access_tokens: [access_token], 258 | days_requested: 20, 259 | options: options 260 | } 261 | ) 262 | asset_report_create_response = 263 | client.asset_report_create(asset_report_create_request) 264 | pretty_print_response(asset_report_create_response.to_hash) 265 | rescue Plaid::ApiError => e 266 | error_response = format_error(e) 267 | pretty_print_response(error_response) 268 | content_type :json 269 | error_response.to_json 270 | end 271 | 272 | asset_report_token = asset_report_create_response.asset_report_token 273 | asset_report_json = nil 274 | num_retries_remaining = 20 275 | while num_retries_remaining.positive? 276 | begin 277 | asset_report_get_request = Plaid::AssetReportGetRequest.new({ asset_report_token: asset_report_token }) 278 | asset_report_get_response = client.asset_report_get(asset_report_get_request) 279 | asset_report_json = asset_report_get_response.report 280 | break 281 | rescue Plaid::ApiError => e 282 | json_response = JSON.parse(e.response_body) 283 | if json_response['error_code'] == 'PRODUCT_NOT_READY' 284 | num_retries_remaining -= 1 285 | sleep(1) 286 | next 287 | end 288 | error_response = format_error(e) 289 | pretty_print_response(error_response) 290 | content_type :json 291 | return error_response.to_json 292 | end 293 | end 294 | 295 | if asset_report_json.nil? 296 | content_type :json 297 | return { 298 | error: { 299 | error_code: 0, 300 | error_message: 'Timed out when polling for Asset Report' 301 | } 302 | }.to_json 303 | end 304 | 305 | asset_report_pdf_get_request = Plaid::AssetReportPDFGetRequest.new({ asset_report_token: asset_report_token }) 306 | asset_report_pdf = client.asset_report_pdf_get( asset_report_pdf_get_request) 307 | 308 | content_type :json 309 | { json: asset_report_json.to_hash, 310 | pdf: Base64.encode64(File.read(asset_report_pdf)) }.to_json 311 | end 312 | 313 | get '/api/statements' do 314 | begin 315 | statements_list_request = Plaid::StatementsListRequest.new( 316 | { 317 | access_token: access_token 318 | } 319 | ) 320 | statements_list_response = 321 | client.statements_list(statements_list_request) 322 | pretty_print_response(statements_list_response.to_hash) 323 | rescue Plaid::ApiError => e 324 | error_response = format_error(e) 325 | pretty_print_response(error_response) 326 | content_type :json 327 | error_response.to_json 328 | end 329 | statement_id = statements_list_response.accounts[0].statements[0].statement_id 330 | statements_download_request = Plaid::StatementsDownloadRequest.new({ access_token: access_token, statement_id: statement_id }) 331 | statement_pdf = client.statements_download(statements_download_request) 332 | 333 | content_type :json 334 | { json: statements_list_response.to_hash, 335 | pdf: Base64.encode64(File.read(statement_pdf)) }.to_json 336 | end 337 | 338 | # rubocop:enable Metrics/BlockLength 339 | 340 | # Retrieve high-level information about an Item 341 | # https://plaid.com/docs/#retrieve-item 342 | get '/api/item' do 343 | begin 344 | item_get_request = Plaid::ItemGetRequest.new({ access_token: access_token}) 345 | item_response = client.item_get(item_get_request) 346 | institutions_get_by_id_request = Plaid::InstitutionsGetByIdRequest.new( 347 | { 348 | institution_id: item_response.item.institution_id, 349 | country_codes: ['US'] 350 | } 351 | ) 352 | institution_response = 353 | client.institutions_get_by_id(institutions_get_by_id_request) 354 | pretty_print_response(item_response.to_hash) 355 | pretty_print_response(institution_response.to_hash) 356 | content_type :json 357 | { item: item_response.item.to_hash, 358 | institution: institution_response.institution.to_hash }.to_json 359 | rescue Plaid::ApiError => e 360 | error_response = format_error(e) 361 | pretty_print_response(error_response) 362 | content_type :json 363 | error_response.to_json 364 | end 365 | end 366 | 367 | # This functionality is only relevant for the ACH Transfer product. 368 | # Retrieve Transfer for a specified Transfer ID 369 | 370 | get '/api/transfer_authorize' do 371 | begin 372 | # We call /accounts/get to obtain first account_id - in production, 373 | # account_id's should be persisted in a data store and retrieved 374 | # from there. 375 | accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token }) 376 | accounts_get_response = client.accounts_get(accounts_get_request) 377 | account_id = accounts_get_response.accounts[0].account_id 378 | 379 | transfer_authorization_create_request = Plaid::TransferAuthorizationCreateRequest.new({ 380 | access_token: access_token, 381 | account_id: account_id, 382 | type: 'debit', 383 | network: 'ach', 384 | amount: '1.00', 385 | ach_class: 'ppd', 386 | user: { 387 | legal_name: 'FirstName LastName', 388 | email_address: 'foobar@email.com', 389 | address: { 390 | street: '123 Main St.', 391 | city: 'San Francisco', 392 | region: 'CA', 393 | postal_code: '94053', 394 | country: 'US' 395 | } 396 | }, 397 | }) 398 | transfer_authorization_create_response = client.transfer_authorization_create(transfer_authorization_create_request) 399 | pretty_print_response(transfer_authorization_create_response.to_hash) 400 | authorization_id = transfer_authorization_create_response.authorization.id 401 | content_type :json 402 | transfer_authorization_create_response.to_hash.to_json 403 | rescue Plaid::ApiError => e 404 | error_response = format_error(e) 405 | pretty_print_response(error_response) 406 | content_type :json 407 | error_response.to_json 408 | end 409 | end 410 | 411 | get '/api/signal_evaluate' do 412 | begin 413 | # We call /accounts/get to obtain first account_id - in production, 414 | # account_id's should be persisted in a data store and retrieved 415 | # from there. 416 | accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token }) 417 | accounts_get_response = client.accounts_get(accounts_get_request) 418 | account_id = accounts_get_response.accounts[0].account_id 419 | 420 | signal_evaluate_request = Plaid::SignalEvaluateRequest.new({ 421 | access_token: access_token, 422 | account_id: account_id, 423 | client_transaction_id: 'tx1234', 424 | amount: 100.00 425 | }) 426 | signal_evaluate_response = client.signal_evaluate(signal_evaluate_request) 427 | pretty_print_response(signal_evaluate_response.to_hash) 428 | content_type :json 429 | signal_evaluate_response.to_hash.to_json 430 | rescue Plaid::ApiError => e 431 | error_response = format_error(e) 432 | pretty_print_response(error_response) 433 | content_type :json 434 | error_response.to_json 435 | end 436 | end 437 | 438 | get '/api/transfer_create' do 439 | begin 440 | transfer_create_request = Plaid::TransferCreateRequest.new({ 441 | access_token: access_token, 442 | account_id: account_id, 443 | authorization_id: authorization_id, 444 | description: 'Debit' 445 | }) 446 | transfer_create_response = client.transfer_create(transfer_create_request) 447 | pretty_print_response(transfer_create_response.to_hash) 448 | content_type :json 449 | transfer_create_response.to_hash.to_json 450 | rescue Plaid::ApiError => e 451 | error_response = format_error(e) 452 | pretty_print_response(error_response) 453 | content_type :json 454 | error_response.to_json 455 | end 456 | end 457 | 458 | # This functionality is only relevant for the UK Payment Initiation product. 459 | # Retrieve Payment for a specified Payment ID 460 | get '/api/payment' do 461 | begin 462 | payment_initiation_payment_get_request = Plaid::PaymentInitiationPaymentGetRequest.new({ payment_id: payment_id}) 463 | payment_get_response = client.payment_initiation_payment_get(payment_initiation_payment_get_request) 464 | pretty_print_response(payment_get_response.to_hash) 465 | content_type :json 466 | { payment: payment_get_response.to_hash}.to_json 467 | rescue Plaid::ApiError => e 468 | error_response = format_error(e) 469 | pretty_print_response(error_response) 470 | content_type :json 471 | error_response.to_json 472 | end 473 | end 474 | 475 | post '/api/create_link_token' do 476 | begin 477 | link_token_create_request = Plaid::LinkTokenCreateRequest.new( 478 | { 479 | user: { client_user_id: 'user-id' }, 480 | client_name: 'Plaid Quickstart', 481 | products: ENV['PLAID_PRODUCTS'].split(','), 482 | country_codes: ENV['PLAID_COUNTRY_CODES'].split(','), 483 | language: 'en', 484 | redirect_uri: nil_if_empty_envvar('PLAID_REDIRECT_URI') 485 | } 486 | ) 487 | if ENV['PLAID_PRODUCTS'].split(',').include?("statements") 488 | today = Date.today 489 | statements = Plaid::LinkTokenCreateRequestStatements.new( 490 | end_date: today, 491 | start_date: today-30 492 | ) 493 | link_token_create_request.statements=statements 494 | end 495 | if products.any? { |product| product.start_with?("cra_") } 496 | link_token_create_request.cra_options = Plaid::LinkTokenCreateRequestCraOptions.new( 497 | days_requested: 60 498 | ) 499 | link_token_create_request.user_token=user_token 500 | link_token_create_request.consumer_report_permissible_purpose =Plaid::ConsumerReportPermissiblePurpose::ACCOUNT_REVIEW_CREDIT 501 | 502 | end 503 | link_response = client.link_token_create(link_token_create_request) 504 | pretty_print_response(link_response.to_hash) 505 | content_type :json 506 | { link_token: link_response.link_token }.to_json 507 | rescue Plaid::ApiError => e 508 | error_response = format_error(e) 509 | pretty_print_response(error_response) 510 | content_type :json 511 | error_response.to_json 512 | end 513 | end 514 | 515 | # Create a user token which can be used for Plaid Check, Income, or Multi-Item link flows 516 | # https://plaid.com/docs/api/users/#usercreate 517 | post '/api/create_user_token' do 518 | begin 519 | request_data = { 520 | # Typically this will be a user ID number from your application. 521 | client_user_id: 'user_' + SecureRandom.uuid 522 | } 523 | 524 | if products.any? { |product| product.start_with?("cra_") } 525 | request_data[:consumer_report_user_identity] = { 526 | first_name: 'Harry', 527 | last_name: 'Potter', 528 | phone_numbers: ['+16174567890'], 529 | emails: ['harrypotter@example.com'], 530 | primary_address: { 531 | city: 'New York', 532 | region: 'NY', 533 | street: '4 Privet Drive', 534 | postal_code: '11111', 535 | country: 'US' 536 | } 537 | } 538 | end 539 | 540 | user = client.user_create(Plaid::UserCreateRequest.new(request_data)) 541 | user_token = user.user_token 542 | content_type :json 543 | user.to_hash.to_json 544 | rescue Plaid::ApiError => e 545 | error_response = format_error(e) 546 | pretty_print_response(error_response) 547 | content_type :json 548 | error_response.to_json 549 | end 550 | end 551 | 552 | def nil_if_empty_envvar(field) 553 | val = ENV[field] 554 | puts "val #{val}" 555 | if !val.nil? && val.length > 0 556 | return val 557 | else 558 | return nil 559 | end 560 | end 561 | 562 | # This functionality is only relevant for the UK/EU Payment Initiation product. 563 | # Sets the payment token in memory on the server side. We generate a new 564 | # payment token so that the developer is not required to supply one. 565 | # This makes the quickstart easier to use. 566 | # See: 567 | # - https://plaid.com/docs/payment-initiation/ 568 | # - https://plaid.com/docs/#payment-initiation-create-link-token-request 569 | post '/api/create_link_token_for_payment' do 570 | begin 571 | payment_initiation_recipient_create_request = Plaid::PaymentInitiationRecipientCreateRequest.new( 572 | { 573 | name: 'Bruce Wayne', 574 | iban: 'GB33BUKB20201555555555', 575 | address: { 576 | street: ['686 Bat Cave Lane'], 577 | city: 'Gotham', 578 | postal_code: '99999', 579 | country: 'GB', 580 | }, 581 | bacs: { 582 | account: '26207729', 583 | sort_code: '560029', 584 | } 585 | } 586 | ) 587 | create_recipient_response = client.payment_initiation_recipient_create( 588 | payment_initiation_recipient_create_request 589 | ) 590 | recipient_id = create_recipient_response.recipient_id 591 | 592 | payment_initiation_recipient_get_request = Plaid::PaymentInitiationRecipientGetRequest.new( 593 | { 594 | recipient_id: recipient_id 595 | } 596 | ) 597 | get_recipient_response = client.payment_initiation_recipient_get( 598 | payment_initiation_recipient_get_request 599 | ) 600 | 601 | payment_initiation_payment_create_request = Plaid::PaymentInitiationPaymentCreateRequest.new( 602 | { 603 | recipient_id: recipient_id, 604 | reference: 'testpayment', 605 | amount: { 606 | value: 100.00, 607 | currency: 'GBP' 608 | } 609 | } 610 | ) 611 | create_payment_response = client.payment_initiation_payment_create( 612 | payment_initiation_payment_create_request 613 | ) 614 | payment_id = create_payment_response.payment_id 615 | 616 | link_token_create_request = Plaid::LinkTokenCreateRequest.new( 617 | { 618 | client_name: 'Plaid Quickstart', 619 | user: { 620 | # This should correspond to a unique id for the current user. 621 | # Typically, this will be a user ID number from your application. 622 | # Personally identifiable information, such as an email address or phone number, should not be used here. 623 | client_user_id: 'user-id' 624 | }, 625 | 626 | # Institutions from all listed countries will be shown. 627 | country_codes: ENV['PLAID_COUNTRY_CODES'].split(','), 628 | language: 'en', 629 | 630 | # The 'payment_initiation' product has to be the only element in the 'products' list. 631 | products: ['payment_initiation'], 632 | 633 | payment_initiation: { 634 | payment_id: payment_id 635 | }, 636 | redirect_uri: nil_if_empty_envvar('PLAID_REDIRECT_URI') 637 | } 638 | ) 639 | link_response = client.link_token_create(link_token_create_request) 640 | pretty_print_response(link_response.to_hash) 641 | content_type :json 642 | { link_token: link_response.link_token }.to_hash.to_json 643 | 644 | rescue Plaid::ApiError => e 645 | error_response = format_error(e) 646 | pretty_print_response(error_response) 647 | content_type :json 648 | error_response.to_json 649 | end 650 | end 651 | 652 | # Retrieve CRA Base Report and PDF 653 | # Base report: https://plaid.com/docs/check/api/#cracheck_reportbase_reportget 654 | # PDF: https://plaid.com/docs/check/api/#cracheck_reportpdfget 655 | get '/api/cra/get_base_report' do 656 | begin 657 | get_response = get_cra_base_report_with_retries(client, user_token) 658 | pretty_print_response(get_response.to_hash) 659 | 660 | pdf_response = client.cra_check_report_pdf_get( 661 | Plaid::CraCheckReportPDFGetRequest.new({ user_token: user_token }) 662 | ) 663 | 664 | content_type :json 665 | { 666 | report: get_response.report.to_hash, 667 | pdf: Base64.encode64(File.read(pdf_response)) 668 | }.to_json 669 | rescue Plaid::ApiError => e 670 | error_response = format_error(e) 671 | pretty_print_response(error_response) 672 | content_type :json 673 | error_response.to_json 674 | end 675 | end 676 | 677 | def get_cra_base_report_with_retries(plaid_client, user_token) 678 | poll_with_retries do 679 | plaid_client.cra_check_report_base_report_get( 680 | Plaid::CraCheckReportBaseReportGetRequest.new({ user_token: user_token }) 681 | ) 682 | end 683 | end 684 | 685 | # Retrieve CRA Income Insights and PDF with Insights 686 | # Income insights: https://plaid.com/docs/check/api/#cracheck_reportincome_insightsget 687 | # PDF w/ income insights: https://plaid.com/docs/check/api/#cracheck_reportpdfget 688 | get '/api/cra/get_income_insights' do 689 | begin 690 | get_response = get_income_insights_with_retries(client, user_token) 691 | pretty_print_response(get_response.to_hash) 692 | 693 | pdf_response = client.cra_check_report_pdf_get( 694 | Plaid::CraCheckReportPDFGetRequest.new({ user_token: user_token, add_ons: [Plaid::CraPDFAddOns::CRA_INCOME_INSIGHTS] }) 695 | ) 696 | 697 | content_type :json 698 | { 699 | report: get_response.report.to_hash, 700 | pdf: Base64.encode64(File.read(pdf_response)) 701 | }.to_json 702 | rescue Plaid::ApiError => e 703 | error_response = format_error(e) 704 | pretty_print_response(error_response) 705 | content_type :json 706 | error_response.to_json 707 | end 708 | end 709 | 710 | def get_income_insights_with_retries(plaid_client, user_token) 711 | poll_with_retries do 712 | plaid_client.cra_check_report_income_insights_get( 713 | Plaid::CraCheckReportIncomeInsightsGetRequest.new({ user_token: user_token }) 714 | ) 715 | end 716 | end 717 | 718 | # Retrieve CRA Partner Insights 719 | # https://plaid.com/docs/check/api/#cracheck_reportpartner_insightsget 720 | get '/api/cra/get_partner_insights' do 721 | begin 722 | response = get_check_partner_insights_with_retries(client, user_token) 723 | pretty_print_response(response.to_hash) 724 | 725 | content_type :json 726 | response.to_hash.to_json 727 | rescue Plaid::ApiError => e 728 | error_response = format_error(e) 729 | pretty_print_response(error_response) 730 | content_type :json 731 | error_response.to_json 732 | end 733 | end 734 | 735 | def get_check_partner_insights_with_retries(plaid_client, user_token) 736 | poll_with_retries do 737 | plaid_client.cra_check_report_partner_insights_get( 738 | Plaid::CraCheckReportPartnerInsightsGetRequest.new({ user_token: user_token }) 739 | ) 740 | end 741 | end 742 | 743 | # Since this quickstart does not support webhooks, this function can be used to poll 744 | # an API that would otherwise be triggered by a webhook. 745 | # For a webhook example, see 746 | # https://github.com/plaid/tutorial-resources or 747 | # https://github.com/plaid/pattern 748 | def poll_with_retries(ms = 1000, retries_left = 20) 749 | begin 750 | yield 751 | rescue Plaid::ApiError => e 752 | if retries_left > 0 753 | sleep(ms / 1000.0) 754 | poll_with_retries(ms, retries_left - 1) { yield } 755 | else 756 | raise 'Ran out of retries while polling' 757 | end 758 | end 759 | end 760 | 761 | def format_error(err) 762 | body = JSON.parse(err.response_body) 763 | { 764 | error: { 765 | status_code: err.code, 766 | error_code: body['error_code'], 767 | error_message: body['error_message'], 768 | error_type: body['error_type'] 769 | } 770 | } 771 | end 772 | 773 | def pretty_print_response(response) 774 | puts JSON.pretty_generate(response) 775 | end 776 | -------------------------------------------------------------------------------- /ruby/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bundle exec ruby app.rb --------------------------------------------------------------------------------