diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index cecca0bb1..b26323396 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -81,7 +81,26 @@ const App = () => {
method: "POST",
});
if (!response.ok) {
- dispatch({ type: "SET_STATE", state: { linkToken: null } });
+ let errorDetail;
+ try {
+ const data = await response.json();
+ errorDetail = data.error || {
+ error_code: data.error_code || "UNKNOWN",
+ error_type: data.error_type || "API_ERROR",
+ error_message:
+ data.error_message || `Request failed with status ${response.status}`,
+ };
+ } catch {
+ errorDetail = {
+ error_code: "UNKNOWN",
+ error_type: "API_ERROR",
+ error_message: `Request failed with status ${response.status}`,
+ };
+ }
+ dispatch({
+ type: "SET_STATE",
+ state: { linkToken: null, linkTokenError: errorDetail },
+ });
return;
}
const data = await response.json();
diff --git a/frontend/src/Components/Error/index.tsx b/frontend/src/Components/Error/index.tsx
index 9c8b04746..b49d49c78 100644
--- a/frontend/src/Components/Error/index.tsx
+++ b/frontend/src/Components/Error/index.tsx
@@ -2,6 +2,8 @@ import React, { useEffect, useState } from "react";
import Button from "plaid-threads/Button";
import Note from "plaid-threads/Note";
+import InlineLink from "plaid-threads/InlineLink";
+
import { ErrorDataItem } from "../../dataUtilities";
import styles from "./index.module.scss";
@@ -68,6 +70,39 @@ const Error = (props: Props) => {
+ {props.error.error_code === "INVALID_LINK_CUSTOMIZATION" && (
+
+
+ Tip: In the{" "}
+
+ dashboard under Link > Link Customization Data Transparency
+ Messaging
+
+ , ensure at least one use case is selected. After selecting a use
+ case, make sure to click Publish Changes .
+
+
+ )}
+ {props.error.error_code === "INSTITUTION_REGISTRATION_REQUIRED" && (
+
+
+ Certain OAuth institutions, including Bank of America, Chase,
+ Capital One, and American Express, may take up to 24 hours to
+ become available after obtaining Production access. PNC and Charles
+ Schwab require a{" "}
+
+ special registration process
+ {" "}
+ to access Production data.
+
+
+ )}
{
isItemAccess,
backend,
linkTokenError,
+ linkExitError,
isPaymentInitiation,
} = useContext(Context);
@@ -78,6 +79,20 @@ const Header = () => {
Error Type: {linkTokenError.error_type}{" "}
Error Message: {linkTokenError.error_message}
+ {linkTokenError.error_code === "INVALID_LINK_CUSTOMIZATION" && (
+
+ Tip: In the{" "}
+
+ dashboard under Link > Link Customization Data
+ Transparency Messaging
+
+ , ensure at least one use case is selected. After selecting a
+ use case, make sure to click Publish Changes .
+
+ )}
) : linkToken === "" ? (
@@ -90,6 +105,62 @@ const Header = () => {
)}
+ {linkExitError != null && (
+
+
+ Link exited with an error.
+
+
+ Error Code: {linkExitError.error_code}
+
+
+ Error Type: {linkExitError.error_type}
+
+ Error Message: {linkExitError.error_message}
+ {linkExitError.display_message && (
+ Details: {linkExitError.display_message}
+ )}
+ {linkExitError.error_code === "INVALID_LINK_CUSTOMIZATION" && (
+
+ Tip: In the{" "}
+
+ dashboard under Link > Link Customization Data
+ Transparency Messaging
+
+ , ensure at least one use case is selected. After selecting a
+ use case, make sure to click Publish Changes .
+
+ )}
+ {linkExitError.error_code ===
+ "INSTITUTION_REGISTRATION_REQUIRED" &&
+ (["PNC", "Charles Schwab"].some((name) =>
+ linkExitError.institution_name
+ ?.toLowerCase()
+ .includes(name.toLowerCase())
+ ) ? (
+
+ {linkExitError.institution_name} requires a special
+ registration process to access Production data. See{" "}
+
+ OAuth institution status
+ {" "}
+ for details.
+
+ ) : (
+
+ Certain OAuth institutions, including Bank of America,
+ Chase, Capital One, and American Express, may take up to 24
+ hours to become available after obtaining Production access.
+
+ ))}
+
+ )}
>
) : (
<>
diff --git a/frontend/src/Components/Link/index.tsx b/frontend/src/Components/Link/index.tsx
index 3b94ac7c7..cd8e02283 100644
--- a/frontend/src/Components/Link/index.tsx
+++ b/frontend/src/Components/Link/index.tsx
@@ -8,6 +8,30 @@ const Link = () => {
const { linkToken, isPaymentInitiation, isCraProductsExclusively, dispatch } =
useContext(Context);
+ const onExit = React.useCallback(
+ (error: any, metadata: any) => {
+ if (error != null) {
+ const linkExitError = {
+ error_type: error.error_type || "",
+ error_code: error.error_code || "",
+ error_message: error.error_message || "",
+ display_message: error.display_message || "",
+ institution_name: metadata?.institution?.name || "",
+ };
+ dispatch({
+ type: "SET_STATE",
+ state: { linkExitError },
+ });
+ fetch("/api/link_exit_error", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(linkExitError),
+ });
+ }
+ },
+ [dispatch]
+ );
+
const onSuccess = React.useCallback(
(public_token: string) => {
// If the access_token is needed, send public_token to server
@@ -61,6 +85,7 @@ const Link = () => {
const config: Parameters[0] = {
token: linkToken!,
onSuccess,
+ onExit,
};
if (window.location.href.includes("?oauth_state_id=")) {
diff --git a/frontend/src/Context/index.tsx b/frontend/src/Context/index.tsx
index 11f44e2ac..7aeb43466 100644
--- a/frontend/src/Context/index.tsx
+++ b/frontend/src/Context/index.tsx
@@ -19,6 +19,13 @@ interface QuickstartState {
error_code: string;
error_type: string;
};
+ linkExitError: {
+ error_message: string;
+ error_code: string;
+ error_type: string;
+ display_message: string;
+ institution_name: string;
+ } | null;
}
const initialState: QuickstartState = {
@@ -40,6 +47,7 @@ const initialState: QuickstartState = {
error_code: "",
error_message: "",
},
+ linkExitError: null,
};
type QuickstartAction = {
diff --git a/go/server.go b/go/server.go
index 999a6984f..bd1542645 100644
--- a/go/server.go
+++ b/go/server.go
@@ -4,6 +4,7 @@ import (
"bufio"
"context"
"encoding/base64"
+ "encoding/json"
"fmt"
"io"
"log"
@@ -98,6 +99,7 @@ func main() {
// 3. Re-initialize with the link token (from step 1) and the full received redirect URI
// from step 2.
+ r.POST("/api/link_exit_error", linkExitError)
r.POST("/api/set_access_token", getAccessToken)
r.POST("/api/create_link_token_for_payment", createLinkTokenForPayment)
r.GET("/api/auth", auth)
@@ -144,13 +146,31 @@ var paymentID string
var authorizationID string
var accountID string
+func linkExitError(c *gin.Context) {
+ var body map[string]interface{}
+ if err := c.BindJSON(&body); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
+ return
+ }
+ jsonBytes, _ := json.MarshalIndent(body, "", " ")
+ fmt.Println("[Link Exit Error (frontend)]")
+ fmt.Println(string(jsonBytes))
+ c.JSON(http.StatusOK, gin.H{"status": "logged"})
+}
+
func renderError(c *gin.Context, originalErr error) {
if plaidError, err := plaid.ToPlaidError(originalErr); err == nil {
- // Return 200 and allow the front end to render the error.
- c.JSON(http.StatusOK, gin.H{"error": plaidError})
+ errorJSON, _ := json.MarshalIndent(plaidError, "", " ")
+ fmt.Println(string(errorJSON))
+ statusCode := int(plaidError.GetStatus())
+ if statusCode == 0 {
+ statusCode = http.StatusInternalServerError
+ }
+ c.JSON(statusCode, gin.H{"error": plaidError})
return
}
+ fmt.Println(originalErr.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": originalErr.Error()})
}
@@ -1057,8 +1077,10 @@ func pollWithRetries[T any](requestCallback func() (T, error), ms int, retriesLe
}
response, err := requestCallback()
if err != nil {
- plaidErr, err := plaid.ToPlaidError(err)
- if plaidErr.ErrorCode != "PRODUCT_NOT_READY" {
+ plaidErr, parseErr := plaid.ToPlaidError(err)
+ isProductNotReady := parseErr == nil && plaidErr.ErrorCode == "PRODUCT_NOT_READY"
+ isServerError := parseErr == nil && plaidErr.HasStatus() && plaidErr.GetStatus() >= 500
+ if !isProductNotReady && !isServerError {
return zero, err
}
time.Sleep(time.Duration(ms) * time.Millisecond)
diff --git a/java/src/main/java/com/plaid/quickstart/PlaidApiException.java b/java/src/main/java/com/plaid/quickstart/PlaidApiException.java
new file mode 100644
index 000000000..601935ba6
--- /dev/null
+++ b/java/src/main/java/com/plaid/quickstart/PlaidApiException.java
@@ -0,0 +1,22 @@
+package com.plaid.quickstart;
+
+import java.util.Map;
+
+public class PlaidApiException extends RuntimeException {
+ private final Map errorResponse;
+ private final int statusCode;
+
+ public PlaidApiException(Map errorResponse, int statusCode) {
+ super(errorResponse.toString());
+ this.errorResponse = errorResponse;
+ this.statusCode = statusCode;
+ }
+
+ public Map getErrorResponse() {
+ return errorResponse;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+}
diff --git a/java/src/main/java/com/plaid/quickstart/PlaidApiExceptionMapper.java b/java/src/main/java/com/plaid/quickstart/PlaidApiExceptionMapper.java
new file mode 100644
index 000000000..d286edf75
--- /dev/null
+++ b/java/src/main/java/com/plaid/quickstart/PlaidApiExceptionMapper.java
@@ -0,0 +1,17 @@
+package com.plaid.quickstart;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+
+public class PlaidApiExceptionMapper implements ExceptionMapper {
+ @Override
+ public Response toResponse(PlaidApiException exception) {
+ System.out.println(exception.getErrorResponse());
+ int statusCode = exception.getStatusCode();
+ return Response.status(statusCode)
+ .entity(exception.getErrorResponse())
+ .type(MediaType.APPLICATION_JSON)
+ .build();
+ }
+}
diff --git a/java/src/main/java/com/plaid/quickstart/PlaidApiHelper.java b/java/src/main/java/com/plaid/quickstart/PlaidApiHelper.java
new file mode 100644
index 000000000..143bf9c30
--- /dev/null
+++ b/java/src/main/java/com/plaid/quickstart/PlaidApiHelper.java
@@ -0,0 +1,38 @@
+package com.plaid.quickstart;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import retrofit2.Call;
+import retrofit2.Response;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class PlaidApiHelper {
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ @SuppressWarnings("unchecked")
+ public static T callPlaid(Call call) throws IOException {
+ Response response = call.execute();
+ if (!response.isSuccessful()) {
+ Map error = new HashMap<>();
+ error.put("status_code", response.code());
+ try {
+ String errorBody = response.errorBody().string();
+ Map body = mapper.readValue(errorBody, Map.class);
+ error.put("error_code", body.getOrDefault("error_code", "UNKNOWN"));
+ error.put("error_type", body.getOrDefault("error_type", "API_ERROR"));
+ error.put("error_message", body.getOrDefault("error_message", errorBody));
+ error.put("display_message", body.get("display_message"));
+ } catch (Exception e) {
+ error.put("error_code", "UNKNOWN");
+ error.put("error_type", "API_ERROR");
+ error.put("error_message", "Unknown error (HTTP " + response.code() + ")");
+ }
+ Map result = new HashMap<>();
+ result.put("error", error);
+ throw new PlaidApiException(result, response.code());
+ }
+ return response.body();
+ }
+}
diff --git a/java/src/main/java/com/plaid/quickstart/QuickstartApplication.java b/java/src/main/java/com/plaid/quickstart/QuickstartApplication.java
index 2c9f6dc61..f37052f36 100644
--- a/java/src/main/java/com/plaid/quickstart/QuickstartApplication.java
+++ b/java/src/main/java/com/plaid/quickstart/QuickstartApplication.java
@@ -14,6 +14,7 @@
import com.plaid.quickstart.resources.InfoResource;
import com.plaid.quickstart.resources.InvestmentTransactionsResource;
import com.plaid.quickstart.resources.ItemResource;
+import com.plaid.quickstart.resources.LinkExitErrorResource;
import com.plaid.quickstart.resources.LinkTokenResource;
import com.plaid.quickstart.resources.LinkTokenWithPaymentResource;
import com.plaid.quickstart.resources.PaymentInitiationResource;
@@ -107,6 +108,7 @@ public void run(final QuickstartConfiguration configuration,
plaidClient = apiClient.createService(PlaidApi.class);
+ environment.jersey().register(new PlaidApiExceptionMapper());
environment.jersey().register(new AccessTokenResource(plaidClient, plaidProducts));
environment.jersey().register(new AccountsResource(plaidClient));
environment.jersey().register(new AssetsResource(plaidClient));
@@ -117,6 +119,7 @@ public void run(final QuickstartConfiguration configuration,
environment.jersey().register(new InfoResource(plaidProducts));
environment.jersey().register(new InvestmentTransactionsResource(plaidClient));
environment.jersey().register(new ItemResource(plaidClient));
+ environment.jersey().register(new LinkExitErrorResource());
environment.jersey().register(new LinkTokenResource(plaidClient, plaidProducts, countryCodes, redirectUri));
environment.jersey().register(new LinkTokenWithPaymentResource(plaidClient, plaidProducts, countryCodes, redirectUri));
environment.jersey().register(new PaymentInitiationResource(plaidClient));
diff --git a/java/src/main/java/com/plaid/quickstart/resources/AccessTokenResource.java b/java/src/main/java/com/plaid/quickstart/resources/AccessTokenResource.java
index 1b3e06ed7..30345f40f 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/AccessTokenResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/AccessTokenResource.java
@@ -5,6 +5,7 @@
import com.plaid.client.request.PlaidApi;
import com.plaid.client.model.ItemPublicTokenExchangeRequest;
import com.plaid.client.model.ItemPublicTokenExchangeResponse;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import com.plaid.client.model.Products;
@@ -19,8 +20,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import retrofit2.Response;
-
@Path("/set_access_token")
@Produces(MediaType.APPLICATION_JSON)
public class AccessTokenResource {
@@ -39,17 +38,15 @@ public InfoResource.InfoResponse getAccessToken(@FormParam("public_token") Strin
ItemPublicTokenExchangeRequest request = new ItemPublicTokenExchangeRequest()
.publicToken(publicToken);
- Response response = plaidClient
- .itemPublicTokenExchange(request)
- .execute();
+ ItemPublicTokenExchangeResponse responseBody = PlaidApiHelper.callPlaid(
+ plaidClient.itemPublicTokenExchange(request));
// Ideally, we would store this somewhere more persistent
- QuickstartApplication.
- accessToken = response.body().getAccessToken();
- QuickstartApplication.itemId = response.body().getItemId();
+ QuickstartApplication.accessToken = responseBody.getAccessToken();
+ QuickstartApplication.itemId = responseBody.getItemId();
LOG.info("public token: " + publicToken);
LOG.info("access token: " + QuickstartApplication.accessToken);
- LOG.info("item ID: " + response.body().getItemId());
+ LOG.info("item ID: " + responseBody.getItemId());
return new InfoResource.InfoResponse(Arrays.asList(), QuickstartApplication.accessToken,
QuickstartApplication.itemId);
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/AccountsResource.java b/java/src/main/java/com/plaid/quickstart/resources/AccountsResource.java
index 9c8681438..dc827406f 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/AccountsResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/AccountsResource.java
@@ -5,6 +5,7 @@
import com.plaid.client.request.PlaidApi;
import com.plaid.client.model.AccountsGetRequest;
import com.plaid.client.model.AccountsGetResponse;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import javax.ws.rs.GET;
@@ -12,8 +13,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/accounts")
@Produces(MediaType.APPLICATION_JSON)
public class AccountsResource {
@@ -28,9 +27,6 @@ public AccountsGetResponse getAccounts() throws IOException {
AccountsGetRequest request = new AccountsGetRequest()
.accessToken(QuickstartApplication.accessToken);
- Response response = plaidClient
- .accountsGet(request)
- .execute();
- return response.body();
+ return PlaidApiHelper.callPlaid(plaidClient.accountsGet(request));
}
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/AssetsResource.java b/java/src/main/java/com/plaid/quickstart/resources/AssetsResource.java
index 61c594280..fb34d9d4d 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/AssetsResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/AssetsResource.java
@@ -9,12 +9,11 @@
import com.plaid.client.model.AssetReportGetRequest;
import com.plaid.client.model.AssetReportGetResponse;
import com.plaid.client.model.AssetReportPDFGetRequest;
+import com.plaid.quickstart.PlaidApiException;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
-import com.plaid.client.model.PlaidError;
-import com.plaid.client.model.PlaidErrorType;
import okhttp3.ResponseBody;
-import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -24,10 +23,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-import com.google.gson.Gson;
-import com.google.common.base.Throwables;
-
@Path("/assets")
@Produces(MediaType.APPLICATION_JSON)
public class AssetsResource {
@@ -46,29 +41,36 @@ public Map getAssetReport() throws IOException {
.accessTokens(accessTokens)
.daysRequested(10);
- Response assetReportCreateResponse = plaidClient
- .assetReportCreate(assetReportCreateRequest)
- .execute();
+ AssetReportCreateResponse assetReportCreateResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.assetReportCreate(assetReportCreateRequest));
- String assetReportToken = assetReportCreateResponse.body().getAssetReportToken();
+ String assetReportToken = assetReportCreateResponseBody.getAssetReportToken();
AssetReportGetRequest assetReportGetRequest = new AssetReportGetRequest()
.assetReportToken(assetReportToken);
- Response assetReportGetResponse = null;
-
- //In a real integration, we would wait for a webhook rather than polling like this
- for (int i = 0; i < 5; i++){
- assetReportGetResponse = plaidClient.assetReportGet(assetReportGetRequest).execute();
- if (assetReportGetResponse.isSuccessful()){
+
+ // In a real integration, we would wait for a webhook rather than polling like this
+ AssetReportGetResponse assetReportGetResponseBody = null;
+ for (int i = 0; i <= 20; i++) {
+ try {
+ assetReportGetResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.assetReportGet(assetReportGetRequest));
break;
- } else {
+ } catch (PlaidApiException e) {
+ @SuppressWarnings("unchecked")
+ Map error = (Map) e.getErrorResponse().get("error");
+ boolean isProductNotReady = error != null && "PRODUCT_NOT_READY".equals(error.get("error_code"));
+ boolean isServerError = error != null && error.get("status_code") instanceof Integer && (Integer) error.get("status_code") >= 500;
+ if (!isProductNotReady && !isServerError) {
+ throw e;
+ }
+ if (i == 20) {
+ throw e;
+ }
try {
- Gson gson = new Gson();
- PlaidError error = gson.fromJson(assetReportGetResponse.errorBody().string(), PlaidError.class);
- error.getErrorType().equals(PlaidErrorType.ASSET_REPORT_ERROR);
- error.getErrorCode().equals("PRODUCT_NOT_READY");
- Thread.sleep(5000);
- } catch (Exception e) {
- throw Throwables.propagate(e);
+ Thread.sleep(1000);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw new IOException("Interrupted while polling for asset report", ie);
}
}
}
@@ -76,13 +78,12 @@ public Map getAssetReport() throws IOException {
AssetReportPDFGetRequest assetReportPDFGetRequest = new AssetReportPDFGetRequest()
.assetReportToken(assetReportToken);
- Response assetReportPDFGetResponse = plaidClient
- .assetReportPdfGet(assetReportPDFGetRequest)
- .execute();
+ ResponseBody assetReportPDFGetResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.assetReportPdfGet(assetReportPDFGetRequest));
- String pdf = Base64.getEncoder().encodeToString(assetReportPDFGetResponse.body().bytes());
+ String pdf = Base64.getEncoder().encodeToString(assetReportPDFGetResponseBody.bytes());
Map responseMap = new HashMap<>();
- responseMap.put("json", assetReportGetResponse.body().getReport());
+ responseMap.put("json", assetReportGetResponseBody.getReport());
responseMap.put("pdf", pdf);
return responseMap;
diff --git a/java/src/main/java/com/plaid/quickstart/resources/AuthResource.java b/java/src/main/java/com/plaid/quickstart/resources/AuthResource.java
index 68682f792..9646c0846 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/AuthResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/AuthResource.java
@@ -5,6 +5,7 @@
import com.plaid.client.request.PlaidApi;
import com.plaid.client.model.AuthGetRequest;
import com.plaid.client.model.AuthGetResponse;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import javax.ws.rs.GET;
@@ -12,8 +13,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
public class AuthResource {
@@ -28,10 +27,6 @@ public AuthGetResponse getAccounts() throws IOException {
AuthGetRequest request = new AuthGetRequest()
.accessToken(QuickstartApplication.accessToken);
- Response response = plaidClient
- .authGet(request)
- .execute();
-
- return response.body();
+ return PlaidApiHelper.callPlaid(plaidClient.authGet(request));
}
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/BalanceResource.java b/java/src/main/java/com/plaid/quickstart/resources/BalanceResource.java
index eb5ff1171..aca2088f6 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/BalanceResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/BalanceResource.java
@@ -7,6 +7,7 @@
import com.plaid.client.request.PlaidApi;
import com.plaid.client.model.AccountsBalanceGetRequest;
import com.plaid.client.model.AccountsGetResponse;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import javax.ws.rs.GET;
@@ -14,8 +15,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/balance")
@Produces(MediaType.APPLICATION_JSON)
public class BalanceResource {
@@ -30,12 +29,11 @@ public Map getAccounts() throws IOException {
AccountsBalanceGetRequest balanceRequest = new AccountsBalanceGetRequest()
.accessToken(QuickstartApplication.accessToken);
- Response balanceResponse = plaidClient
- .accountsBalanceGet(balanceRequest)
- .execute();
+ AccountsGetResponse balanceResponse = PlaidApiHelper.callPlaid(
+ plaidClient.accountsBalanceGet(balanceRequest));
Map response = new HashMap<>();
- response.put("accounts", balanceResponse.body().getAccounts());
+ response.put("accounts", balanceResponse.getAccounts());
return response;
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/CraResource.java b/java/src/main/java/com/plaid/quickstart/resources/CraResource.java
index 639f2a323..3f2596bbe 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/CraResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/CraResource.java
@@ -10,6 +10,8 @@
import com.plaid.client.model.CraCheckReportPartnerInsightsGetResponse;
import com.plaid.client.model.CraPDFAddOns;
import com.plaid.client.request.PlaidApi;
+import com.plaid.quickstart.PlaidApiException;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import com.google.common.base.Throwables;
@@ -17,6 +19,8 @@
import retrofit2.Call;
import retrofit2.Response;
+import java.util.Map;
+
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@@ -49,8 +53,7 @@ public Map getBaseReport() throws IOException {
request.setUserId(QuickstartApplication.userId);
}
CraCheckReportBaseReportGetResponse baseReportResponse = pollWithRetries(
- plaidClient.craCheckReportBaseReportGet(request)
- ).body();
+ plaidClient.craCheckReportBaseReportGet(request));
CraCheckReportPDFGetRequest pdfRequest = new CraCheckReportPDFGetRequest();
// Use user_token if available, otherwise use user_id
@@ -59,9 +62,10 @@ public Map getBaseReport() throws IOException {
} else if (QuickstartApplication.userId != null) {
pdfRequest.setUserId(QuickstartApplication.userId);
}
- Response pdfResponse = plaidClient.craCheckReportPdfGet(pdfRequest).execute();
+ ResponseBody pdfResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.craCheckReportPdfGet(pdfRequest));
- String pdfBase64 = Base64.getEncoder().encodeToString(pdfResponse.body().bytes());
+ String pdfBase64 = Base64.getEncoder().encodeToString(pdfResponseBody.bytes());
Map responseMap = new HashMap<>();
responseMap.put("report", baseReportResponse.getReport());
@@ -84,9 +88,8 @@ public Map getIncomeInsigts() throws IOException {
} else if (QuickstartApplication.userId != null) {
request.setUserId(QuickstartApplication.userId);
}
- CraCheckReportIncomeInsightsGetResponse baseReportResponse = pollWithRetries(
- plaidClient.craCheckReportIncomeInsightsGet(request)
- ).body();
+ CraCheckReportIncomeInsightsGetResponse incomeInsightsResponse = pollWithRetries(
+ plaidClient.craCheckReportIncomeInsightsGet(request));
CraCheckReportPDFGetRequest pdfRequest = new CraCheckReportPDFGetRequest();
// Use user_token if available, otherwise use user_id
@@ -96,12 +99,13 @@ public Map getIncomeInsigts() throws IOException {
pdfRequest.setUserId(QuickstartApplication.userId);
}
pdfRequest.addAddOnsItem(CraPDFAddOns.INCOME_INSIGHTS);
- Response pdfResponse = plaidClient.craCheckReportPdfGet(pdfRequest).execute();
+ ResponseBody pdfResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.craCheckReportPdfGet(pdfRequest));
- String pdfBase64 = Base64.getEncoder().encodeToString(pdfResponse.body().bytes());
+ String pdfBase64 = Base64.getEncoder().encodeToString(pdfResponseBody.bytes());
Map responseMap = new HashMap<>();
- responseMap.put("report", baseReportResponse.getReport());
+ responseMap.put("report", incomeInsightsResponse.getReport());
responseMap.put("pdf", pdfBase64);
return responseMap;
}
@@ -118,7 +122,7 @@ public CraCheckReportPartnerInsightsGetResponse getPartnerInsigts() throws IOExc
} else if (QuickstartApplication.userId != null) {
request.setUserId(QuickstartApplication.userId);
}
- return pollWithRetries(plaidClient.craCheckReportPartnerInsightsGet(request)).body();
+ return pollWithRetries(plaidClient.craCheckReportPartnerInsightsGet(request));
}
// Since this quickstart does not support webhooks, this function can be used to
@@ -127,22 +131,28 @@ public CraCheckReportPartnerInsightsGetResponse getPartnerInsigts() throws IOExc
// For a webhook example, see
// https://github.com/plaid/tutorial-resources or
// https://github.com/plaid/pattern
- private Response pollWithRetries(Call requestCallback) throws IOException {
+ private T pollWithRetries(Call requestCallback) throws IOException {
for (int i = 0; i <= 20; i++) {
// Clone the call for each retry since Retrofit calls can only be executed once
Call call = i == 0 ? requestCallback : requestCallback.clone();
- Response response = call.execute();
-
- if (response.isSuccessful()) {
- return response;
- } else {
+ try {
+ return PlaidApiHelper.callPlaid(call);
+ } catch (PlaidApiException e) {
+ Map error = (Map) e.getErrorResponse().get("error");
+ boolean isProductNotReady = error != null && "PRODUCT_NOT_READY".equals(error.get("error_code"));
+ boolean isServerError = error != null && error.get("status_code") instanceof Integer && (Integer) error.get("status_code") >= 500;
+ if (!isProductNotReady && !isServerError) {
+ throw e;
+ }
+ if (i == 20) {
+ throw e;
+ }
try {
- Thread.sleep(5000);
- } catch (Exception e) {
- throw Throwables.propagate(e);
+ Thread.sleep(1000);
+ } catch (InterruptedException ie) {
+ throw Throwables.propagate(ie);
}
}
-
}
throw Throwables.propagate(new Exception("Ran out of retries while polling"));
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/HoldingsResource.java b/java/src/main/java/com/plaid/quickstart/resources/HoldingsResource.java
index 610b48dc4..6c7d0c81e 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/HoldingsResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/HoldingsResource.java
@@ -7,6 +7,7 @@
import com.plaid.client.request.PlaidApi;
import com.plaid.client.model.InvestmentsHoldingsGetRequest;
import com.plaid.client.model.InvestmentsHoldingsGetResponse;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import javax.ws.rs.GET;
@@ -14,8 +15,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/holdings")
@Produces(MediaType.APPLICATION_JSON)
public class HoldingsResource {
@@ -31,11 +30,10 @@ public HoldingsResponse getAccounts() throws IOException {
InvestmentsHoldingsGetRequest request = new InvestmentsHoldingsGetRequest()
.accessToken(QuickstartApplication.accessToken);
- Response response = plaidClient
- .investmentsHoldingsGet(request)
- .execute();
+ InvestmentsHoldingsGetResponse responseBody = PlaidApiHelper.callPlaid(
+ plaidClient.investmentsHoldingsGet(request));
- return new HoldingsResponse(response.body());
+ return new HoldingsResponse(responseBody);
}
private static class HoldingsResponse {
diff --git a/java/src/main/java/com/plaid/quickstart/resources/IdentityResource.java b/java/src/main/java/com/plaid/quickstart/resources/IdentityResource.java
index ec4d97a0f..0b88ef7d5 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/IdentityResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/IdentityResource.java
@@ -7,6 +7,7 @@
import com.plaid.client.model.AccountIdentity;
import com.plaid.client.model.IdentityGetRequest;
import com.plaid.client.model.IdentityGetResponse;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import java.util.List;
@@ -15,8 +16,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/identity")
@Produces(MediaType.APPLICATION_JSON)
public class IdentityResource {
@@ -30,10 +29,9 @@ public IdentityResource(PlaidApi plaidClient) {
public IdentityResponse getAccounts() throws IOException {
IdentityGetRequest request = new IdentityGetRequest()
.accessToken(QuickstartApplication.accessToken);
- Response response = plaidClient
- .identityGet(request)
- .execute();
- return new IdentityResponse(response.body());
+ IdentityGetResponse responseBody = PlaidApiHelper.callPlaid(
+ plaidClient.identityGet(request));
+ return new IdentityResponse(responseBody);
}
private static class IdentityResponse {
diff --git a/java/src/main/java/com/plaid/quickstart/resources/InvestmentTransactionsResource.java b/java/src/main/java/com/plaid/quickstart/resources/InvestmentTransactionsResource.java
index 26e577693..aaadccdaf 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/InvestmentTransactionsResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/InvestmentTransactionsResource.java
@@ -7,6 +7,7 @@
import com.plaid.client.model.InvestmentsTransactionsGetRequest;
import com.plaid.client.model.InvestmentsTransactionsGetResponse;
import com.plaid.client.model.InvestmentsTransactionsGetRequestOptions;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import java.io.IOException;
@@ -16,8 +17,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/investments_transactions")
@Produces(MediaType.APPLICATION_JSON)
public class InvestmentTransactionsResource {
@@ -40,10 +39,9 @@ public InvestmentTransactionsResponse getAccounts() throws IOException {
.endDate(endDate)
.options(options);
- Response response = plaidClient
- .investmentsTransactionsGet(request)
- .execute();
- return new InvestmentTransactionsResponse(response.body());
+ InvestmentsTransactionsGetResponse responseBody = PlaidApiHelper.callPlaid(
+ plaidClient.investmentsTransactionsGet(request));
+ return new InvestmentTransactionsResponse(responseBody);
}
private static class InvestmentTransactionsResponse {
diff --git a/java/src/main/java/com/plaid/quickstart/resources/ItemResource.java b/java/src/main/java/com/plaid/quickstart/resources/ItemResource.java
index 4906dc904..713a79772 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/ItemResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/ItemResource.java
@@ -12,6 +12,7 @@
import com.plaid.client.model.InstitutionsGetByIdResponse;
import com.plaid.client.model.Institution;
import com.plaid.client.model.ItemWithConsentFields;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import javax.ws.rs.GET;
@@ -19,8 +20,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/item")
@Produces(MediaType.APPLICATION_JSON)
public class ItemResource {
@@ -35,21 +34,19 @@ public ItemResponse getItem() throws IOException {
ItemGetRequest request = new ItemGetRequest()
.accessToken(QuickstartApplication.accessToken);
- Response itemResponse = plaidClient
- .itemGet(request)
- .execute();
-
+ ItemGetResponse itemResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.itemGet(request));
+
InstitutionsGetByIdRequest institutionsRequest = new InstitutionsGetByIdRequest()
- .institutionId(itemResponse.body().getItem().getInstitutionId())
+ .institutionId(itemResponseBody.getItem().getInstitutionId())
.addCountryCodesItem(CountryCode.US);
- Response institutionsResponse = plaidClient
- .institutionsGetById(institutionsRequest)
- .execute();
+ InstitutionsGetByIdResponse institutionsResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.institutionsGetById(institutionsRequest));
return new ItemResponse(
- itemResponse.body().getItem(),
- institutionsResponse.body().getInstitution()
+ itemResponseBody.getItem(),
+ institutionsResponseBody.getInstitution()
);
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/LinkExitErrorResource.java b/java/src/main/java/com/plaid/quickstart/resources/LinkExitErrorResource.java
new file mode 100644
index 000000000..84ea2bbb9
--- /dev/null
+++ b/java/src/main/java/com/plaid/quickstart/resources/LinkExitErrorResource.java
@@ -0,0 +1,28 @@
+package com.plaid.quickstart.resources;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.util.Map;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+@Path("/link_exit_error")
+@Consumes(MediaType.APPLICATION_JSON)
+@Produces(MediaType.APPLICATION_JSON)
+public class LinkExitErrorResource {
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ @POST
+ public Map logLinkExitError(Map body) {
+ System.out.println("[Link Exit Error (frontend)]");
+ try {
+ System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body));
+ } catch (Exception e) {
+ System.out.println(body);
+ }
+ return Map.of("status", "logged");
+ }
+}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/LinkTokenResource.java b/java/src/main/java/com/plaid/quickstart/resources/LinkTokenResource.java
index f285b7f1b..803b3be95 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/LinkTokenResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/LinkTokenResource.java
@@ -10,8 +10,8 @@
import com.plaid.client.model.LinkTokenCreateResponse;
import com.plaid.client.model.Products;
import com.plaid.client.request.PlaidApi;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
-import retrofit2.Response;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
@@ -20,7 +20,6 @@
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -48,7 +47,6 @@ public static class LinkToken {
@JsonProperty
private String linkToken;
-
public LinkToken(String linkToken) {
this.linkToken = linkToken;
}
@@ -101,9 +99,8 @@ public LinkToken(String linkToken) {
request.craOptions(options);
}
- Response response =plaidClient
- .linkTokenCreate(request)
- .execute();
- return new LinkToken(response.body().getLinkToken());
+ LinkTokenCreateResponse responseBody = PlaidApiHelper.callPlaid(
+ plaidClient.linkTokenCreate(request));
+ return new LinkToken(responseBody.getLinkToken());
}
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/LinkTokenWithPaymentResource.java b/java/src/main/java/com/plaid/quickstart/resources/LinkTokenWithPaymentResource.java
index d74139159..41b93ac49 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/LinkTokenWithPaymentResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/LinkTokenWithPaymentResource.java
@@ -16,6 +16,7 @@
import com.plaid.client.model.PaymentInitiationAddress;
import com.plaid.client.model.PaymentInitiationRecipientCreateRequest;
import com.plaid.client.model.LinkTokenCreateRequestPaymentInitiation;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import java.util.Arrays;
@@ -27,8 +28,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/create_link_token_for_payment")
@Produces(MediaType.APPLICATION_JSON)
public class LinkTokenWithPaymentResource {
@@ -55,13 +54,10 @@ public LinkTokenWithPaymentResource(PlaidApi plaidClient, List plaidProd
.address(address);
- Response recipientResponse =
- this.plaidClient
- .paymentInitiationRecipientCreate(recipientCreateRequest)
- .execute();
-
+ PaymentInitiationRecipientCreateResponse recipientResponseBody = PlaidApiHelper.callPlaid(
+ this.plaidClient.paymentInitiationRecipientCreate(recipientCreateRequest));
- String recipientId = recipientResponse.body().getRecipientId();
+ String recipientId = recipientResponseBody.getRecipientId();
PaymentAmount amount = new PaymentAmount()
.currency(PaymentAmountCurrency.GBP)
@@ -72,12 +68,10 @@ public LinkTokenWithPaymentResource(PlaidApi plaidClient, List plaidProd
.reference("reference")
.amount(amount);
- Response paymentResponse = plaidClient
- .paymentInitiationPaymentCreate(paymentCreateRequest)
- .execute();
+ PaymentInitiationPaymentCreateResponse paymentResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.paymentInitiationPaymentCreate(paymentCreateRequest));
-
- String paymentId = paymentResponse.body().getPaymentId();
+ String paymentId = paymentResponseBody.getPaymentId();
QuickstartApplication.paymentId = paymentId;
LinkTokenCreateRequestPaymentInitiation paymentInitiation = new LinkTokenCreateRequestPaymentInitiation()
@@ -101,10 +95,8 @@ public LinkTokenWithPaymentResource(PlaidApi plaidClient, List plaidProd
.redirectUri(this.redirectUri)
.paymentInitiation(paymentInitiation);
- Response response =plaidClient
- .linkTokenCreate(request)
- .execute();
-
- return new LinkTokenResource.LinkToken(response.body().getLinkToken());
+ LinkTokenCreateResponse responseBody = PlaidApiHelper.callPlaid(
+ plaidClient.linkTokenCreate(request));
+ return new LinkTokenResource.LinkToken(responseBody.getLinkToken());
}
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/PaymentInitiationResource.java b/java/src/main/java/com/plaid/quickstart/resources/PaymentInitiationResource.java
index a7e5cfe11..c5ddc6568 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/PaymentInitiationResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/PaymentInitiationResource.java
@@ -3,11 +3,11 @@
import java.io.IOException;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.gson.Gson;
import com.plaid.client.request.PlaidApi;
import com.plaid.client.model.PaymentInitiationPaymentGetRequest;
import com.plaid.client.model.PaymentInitiationPaymentGetResponse;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import javax.ws.rs.GET;
@@ -15,17 +15,10 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import retrofit2.Response;
-
// This functionality is only relevant for the UK Payment Initiation product.
@Path("/payment")
@Produces(MediaType.APPLICATION_JSON)
public class PaymentInitiationResource {
- private static final Logger LOG = LoggerFactory.getLogger(PaymentInitiationResource.class);
-
private final PlaidApi plaidClient;
public PaymentInitiationResource(PlaidApi plaidClient) {
@@ -39,20 +32,9 @@ public PaymentResponse getPayment() throws IOException {
PaymentInitiationPaymentGetRequest request = new PaymentInitiationPaymentGetRequest()
.paymentId(paymentId);
- Response response =
- plaidClient
- .paymentInitiationPaymentGet(request)
- .execute();
- if (!response.isSuccessful()) {
- try {
- Gson gson = new Gson();
- Error errorResponse = gson.fromJson(response.errorBody().string(), Error.class);
- LOG.error("error: " + errorResponse);
- } catch (Exception e) {
- LOG.error("error", e);
- }
- }
- return new PaymentResponse(response.body());
+ PaymentInitiationPaymentGetResponse responseBody = PlaidApiHelper.callPlaid(
+ plaidClient.paymentInitiationPaymentGet(request));
+ return new PaymentResponse(responseBody);
}
private static class PaymentResponse {
diff --git a/java/src/main/java/com/plaid/quickstart/resources/PublicTokenResource.java b/java/src/main/java/com/plaid/quickstart/resources/PublicTokenResource.java
index 197f15c38..6b5980079 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/PublicTokenResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/PublicTokenResource.java
@@ -6,6 +6,7 @@
import com.plaid.client.request.PlaidApi;
import com.plaid.client.model.ItemPublicTokenCreateRequest;
import com.plaid.client.model.ItemPublicTokenCreateResponse;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import javax.ws.rs.GET;
@@ -13,8 +14,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/create_public_token")
@Produces(MediaType.APPLICATION_JSON)
public class PublicTokenResource {
@@ -30,10 +29,6 @@ public ItemPublicTokenCreateResponse createPublicToken() throws IOException {
ItemPublicTokenCreateRequest request = new ItemPublicTokenCreateRequest()
.accessToken(QuickstartApplication.accessToken);
- Response response = plaidClient
- .itemCreatePublicToken(request)
- .execute();
-
- return response.body();
+ return PlaidApiHelper.callPlaid(plaidClient.itemCreatePublicToken(request));
}
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/SignalResource.java b/java/src/main/java/com/plaid/quickstart/resources/SignalResource.java
index 49a221b6b..9c6631573 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/SignalResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/SignalResource.java
@@ -9,6 +9,7 @@
import com.plaid.client.model.AccountIdentity;
import com.plaid.client.model.SignalEvaluateRequest;
import com.plaid.client.model.SignalEvaluateResponse;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import java.util.List;
@@ -17,8 +18,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/signal_evaluate")
@Produces(MediaType.APPLICATION_JSON)
public class SignalResource {
@@ -35,11 +34,10 @@ public SignalEvaluateResponse signalEvaluate() throws IOException {
AccountsGetRequest accountsGetRequest = new AccountsGetRequest()
.accessToken(QuickstartApplication.accessToken);
- Response accountsGetResponse = plaidClient
- .accountsGet(accountsGetRequest)
- .execute();
+ AccountsGetResponse accountsGetResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.accountsGet(accountsGetRequest));
- QuickstartApplication.accountId = accountsGetResponse.body().getAccounts().get(0).getAccountId();
+ QuickstartApplication.accountId = accountsGetResponseBody.getAccounts().get(0).getAccountId();
// Generate unique transaction ID using timestamp and random component
String clientTransactionId = String.format("txn-%d-%s",
@@ -56,11 +54,7 @@ public SignalEvaluateResponse signalEvaluate() throws IOException {
signalEvaluateRequest.rulesetKey(signalRulesetKey);
}
- Response signalEvaluateResponse = plaidClient
- .signalEvaluate(signalEvaluateRequest)
- .execute();
-
- return signalEvaluateResponse.body();
+ return PlaidApiHelper.callPlaid(plaidClient.signalEvaluate(signalEvaluateRequest));
}
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/StatementsResource.java b/java/src/main/java/com/plaid/quickstart/resources/StatementsResource.java
index ce21bf813..25c40e476 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/StatementsResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/StatementsResource.java
@@ -7,6 +7,7 @@
import com.plaid.client.model.StatementsListRequest;
import com.plaid.client.model.StatementsListResponse;
import com.plaid.client.model.StatementsDownloadRequest;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import okhttp3.ResponseBody;
@@ -19,8 +20,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/statements")
@Produces(MediaType.APPLICATION_JSON)
public class StatementsResource {
@@ -36,22 +35,20 @@ public Map statementsList() throws IOException {
StatementsListRequest statementsListRequest = new StatementsListRequest()
.accessToken(QuickstartApplication.accessToken);
- Response statementsListResponse = plaidClient
- .statementsList(statementsListRequest)
- .execute();
+ StatementsListResponse statementsListResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.statementsList(statementsListRequest));
StatementsDownloadRequest statementsDownloadRequest = new StatementsDownloadRequest()
.accessToken(QuickstartApplication.accessToken)
- .statementId(statementsListResponse.body().getAccounts().get(0).getStatements().get(0).getStatementId());
-
- Response statementsDownloadResponse = plaidClient
- .statementsDownload(statementsDownloadRequest)
- .execute();
+ .statementId(statementsListResponseBody.getAccounts().get(0).getStatements().get(0).getStatementId());
+
+ ResponseBody statementsDownloadResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.statementsDownload(statementsDownloadRequest));
- String pdf = Base64.getEncoder().encodeToString(statementsDownloadResponse.body().bytes());
+ String pdf = Base64.getEncoder().encodeToString(statementsDownloadResponseBody.bytes());
Map responseMap = new HashMap<>();
- responseMap.put("json", statementsListResponse.body());
+ responseMap.put("json", statementsListResponseBody);
responseMap.put("pdf", pdf);
return responseMap;
diff --git a/java/src/main/java/com/plaid/quickstart/resources/TransactionsResource.java b/java/src/main/java/com/plaid/quickstart/resources/TransactionsResource.java
index 198754ed4..c88122db5 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/TransactionsResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/TransactionsResource.java
@@ -12,6 +12,7 @@
import com.plaid.client.model.TransactionsSyncResponse;
import com.plaid.client.model.Transaction;
import com.plaid.client.model.RemovedTransaction;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import javax.ws.rs.GET;
@@ -19,8 +20,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import retrofit2.Response;
-
@Path("/transactions")
@Produces(MediaType.APPLICATION_JSON)
public class TransactionsResource {
@@ -47,8 +46,8 @@ public TransactionsResponse getTransactions() throws IOException, InterruptedExc
.accessToken(QuickstartApplication.accessToken)
.cursor(cursor);
- Response response = plaidClient.transactionsSync(request).execute();
- TransactionsSyncResponse responseBody = response.body();
+ TransactionsSyncResponse responseBody = PlaidApiHelper.callPlaid(
+ plaidClient.transactionsSync(request));
cursor = responseBody.getNextCursor();
diff --git a/java/src/main/java/com/plaid/quickstart/resources/TransferAuthorizeResource.java b/java/src/main/java/com/plaid/quickstart/resources/TransferAuthorizeResource.java
index 19e889036..abb832b4b 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/TransferAuthorizeResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/TransferAuthorizeResource.java
@@ -5,6 +5,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.plaid.client.request.PlaidApi;
import com.plaid.client.model.Transfer;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import com.plaid.client.model.TransferAuthorizationUserInRequest;
import com.plaid.client.model.TransferAuthorizationCreateRequest;
@@ -21,11 +22,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import retrofit2.Response;
-
@Path("/transfer_authorize")
@Produces(MediaType.APPLICATION_JSON)
public class TransferAuthorizeResource {
@@ -40,11 +36,10 @@ public TransferAuthorizationCreateResponse authorizeTransfer() throws IOExceptio
AccountsGetRequest accountsGetRequest = new AccountsGetRequest()
.accessToken(QuickstartApplication.accessToken);
- Response accountsGetResponse = plaidClient
- .accountsGet(accountsGetRequest)
- .execute();
+ AccountsGetResponse accountsGetResponseBody = PlaidApiHelper.callPlaid(
+ plaidClient.accountsGet(accountsGetRequest));
- QuickstartApplication.accountId = accountsGetResponse.body().getAccounts().get(0).getAccountId();
+ QuickstartApplication.accountId = accountsGetResponseBody.getAccounts().get(0).getAccountId();
TransferAuthorizationUserInRequest user = new TransferAuthorizationUserInRequest()
.legalName("FirstName LastName");
@@ -58,11 +53,10 @@ public TransferAuthorizationCreateResponse authorizeTransfer() throws IOExceptio
.achClass(ACHClass.PPD)
.user(user);
- Response transferAuthorizationCreateResponse = plaidClient
- .transferAuthorizationCreate(transferAuthorizationCreateRequest)
- .execute();
+ TransferAuthorizationCreateResponse responseBody = PlaidApiHelper.callPlaid(
+ plaidClient.transferAuthorizationCreate(transferAuthorizationCreateRequest));
- QuickstartApplication.authorizationId = transferAuthorizationCreateResponse.body().getAuthorization().getId();
- return transferAuthorizationCreateResponse.body();
+ QuickstartApplication.authorizationId = responseBody.getAuthorization().getId();
+ return responseBody;
}
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/TransferCreateResource.java b/java/src/main/java/com/plaid/quickstart/resources/TransferCreateResource.java
index 5e7175638..06d01eab8 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/TransferCreateResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/TransferCreateResource.java
@@ -5,6 +5,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.plaid.client.request.PlaidApi;
import com.plaid.client.model.Transfer;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
import com.plaid.client.model.TransferCreateRequest;
import com.plaid.client.model.TransferCreateResponse;
@@ -15,11 +16,6 @@
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import retrofit2.Response;
-
@Path("/transfer_create")
@Produces(MediaType.APPLICATION_JSON)
public class TransferCreateResource {
@@ -36,9 +32,6 @@ public TransferCreateResponse createTransfer() throws IOException {
.accessToken(QuickstartApplication.accessToken)
.accountId(QuickstartApplication.accountId)
.description("Debit");
- Response transferCreateResponse = plaidClient
- .transferCreate(request)
- .execute();
- return transferCreateResponse.body();
+ return PlaidApiHelper.callPlaid(plaidClient.transferCreate(request));
}
}
diff --git a/java/src/main/java/com/plaid/quickstart/resources/UserTokenResource.java b/java/src/main/java/com/plaid/quickstart/resources/UserTokenResource.java
index 0a32cffd5..29c61b6d0 100644
--- a/java/src/main/java/com/plaid/quickstart/resources/UserTokenResource.java
+++ b/java/src/main/java/com/plaid/quickstart/resources/UserTokenResource.java
@@ -10,8 +10,9 @@
import com.plaid.client.model.UserCreateRequest;
import com.plaid.client.model.UserCreateResponse;
import com.plaid.client.request.PlaidApi;
+import com.plaid.quickstart.PlaidApiException;
+import com.plaid.quickstart.PlaidApiHelper;
import com.plaid.quickstart.QuickstartApplication;
-import retrofit2.Response;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
@@ -76,72 +77,48 @@ public UserCreateResponse createUserToken() throws IOException {
userCreateRequest.identity(identity);
}
+ UserCreateResponse responseBody;
try {
- Response userResponse = plaidClient.userCreate(userCreateRequest, null).execute();
-
- // Check if the response was successful
- if (!userResponse.isSuccessful()) {
- System.err.println("User create failed with code: " + userResponse.code());
- if (userResponse.errorBody() != null) {
- String errorBody = userResponse.errorBody().string();
- System.err.println("Error body: " + errorBody);
-
- if (errorBody.contains("INVALID_FIELD") &&
- plaidProducts.stream().anyMatch(product -> product.startsWith("cra_"))) {
- UserCreateRequest retryRequest = new UserCreateRequest()
- .clientUserId(clientUserId);
-
- AddressData addressData = new AddressData()
- .city("New York")
- .region("NY")
- .street("4 Privet Drive")
- .postalCode("11111")
- .country("US");
-
- retryRequest.consumerReportUserIdentity(new ConsumerReportUserIdentity()
- .dateOfBirth(LocalDate.parse("1980-07-31"))
- .firstName("Harry")
- .lastName("Potter")
- .phoneNumbers(Arrays.asList("+16174567890"))
- .emails(List.of("harrypotter@example.com"))
- .primaryAddress(addressData));
-
- Response retryResponse = plaidClient.userCreate(retryRequest, null).execute();
-
- // Check if the response was successful
- if (!retryResponse.isSuccessful()) {
- System.err.println("User create (retry) failed with code: " + retryResponse.code());
- if (retryResponse.errorBody() != null) {
- System.err.println("Error body: " + retryResponse.errorBody().string());
- }
- throw new IOException("User create (retry) failed: " + retryResponse.code());
- }
-
- // Store both user_token and user_id
- if (retryResponse.body().getUserToken() != null) {
- QuickstartApplication.userToken = retryResponse.body().getUserToken();
- }
- if (retryResponse.body().getUserId() != null) {
- QuickstartApplication.userId = retryResponse.body().getUserId();
- }
-
- return retryResponse.body();
- }
- }
- throw new IOException("User create failed: " + userResponse.code());
- }
-
- // Store both user_token and user_id
- if (userResponse.body().getUserToken() != null) {
- QuickstartApplication.userToken = userResponse.body().getUserToken();
- }
- if (userResponse.body().getUserId() != null) {
- QuickstartApplication.userId = userResponse.body().getUserId();
+ responseBody = PlaidApiHelper.callPlaid(
+ plaidClient.userCreate(userCreateRequest, null));
+ } catch (PlaidApiException e) {
+ // If the error is INVALID_FIELD for CRA products, retry with legacy identity format
+ String errorMessage = e.getErrorResponse().toString();
+ if (errorMessage.contains("INVALID_FIELD") &&
+ plaidProducts.stream().anyMatch(product -> product.startsWith("cra_"))) {
+ UserCreateRequest retryRequest = new UserCreateRequest()
+ .clientUserId(clientUserId);
+
+ AddressData addressData = new AddressData()
+ .city("New York")
+ .region("NY")
+ .street("4 Privet Drive")
+ .postalCode("11111")
+ .country("US");
+
+ retryRequest.consumerReportUserIdentity(new ConsumerReportUserIdentity()
+ .dateOfBirth(LocalDate.parse("1980-07-31"))
+ .firstName("Harry")
+ .lastName("Potter")
+ .phoneNumbers(Arrays.asList("+16174567890"))
+ .emails(List.of("harrypotter@example.com"))
+ .primaryAddress(addressData));
+
+ responseBody = PlaidApiHelper.callPlaid(
+ plaidClient.userCreate(retryRequest, null));
+ } else {
+ throw e;
}
+ }
- return userResponse.body();
- } catch (IOException e) {
- throw e;
+ // Store both user_token and user_id
+ if (responseBody.getUserToken() != null) {
+ QuickstartApplication.userToken = responseBody.getUserToken();
+ }
+ if (responseBody.getUserId() != null) {
+ QuickstartApplication.userId = responseBody.getUserId();
}
+
+ return responseBody;
}
}
\ No newline at end of file
diff --git a/node/index.js b/node/index.js
index 90dca9236..6b36641d0 100644
--- a/node/index.js
+++ b/node/index.js
@@ -870,23 +870,40 @@ const pollWithRetries = (
new Promise((resolve, reject) => {
requestCallback()
.then(resolve)
- .catch(() => {
+ .catch((error) => {
+ const errorCode = error?.response?.data?.error_code;
+ const statusCode = error?.response?.status;
+ const isRetryable = errorCode === 'PRODUCT_NOT_READY' || (statusCode >= 500 && statusCode < 600);
+ if (!isRetryable) {
+ reject(error);
+ return;
+ }
+ if (retriesLeft === 1) {
+ reject('Ran out of retries while polling');
+ return;
+ }
setTimeout(() => {
- if (retriesLeft === 1) {
- reject('Ran out of retries while polling');
- return;
- }
pollWithRetries(
requestCallback,
ms,
retriesLeft - 1,
- ).then(resolve);
+ ).then(resolve).catch(reject);
}, ms);
});
});
+app.post('/api/link_exit_error', function (request, response, next) {
+ console.log('[Link Exit Error (frontend)]');
+ console.log(util.inspect(request.body, { colors: true, depth: 4 }));
+ response.json({ status: 'logged' });
+});
+
app.use('/api', function (error, request, response, next) {
- console.log(error);
- prettyPrintResponse(error.response);
- response.json(formatError(error.response));
+ if (error.response?.data) {
+ prettyPrintResponse(error.response);
+ } else {
+ console.log(error.message || error);
+ }
+ const statusCode = error.response?.status || 500;
+ response.status(statusCode).json(formatError(error.response));
});
\ No newline at end of file
diff --git a/python/server.py b/python/server.py
index e8a34a4fb..4284ad6be 100644
--- a/python/server.py
+++ b/python/server.py
@@ -148,119 +148,112 @@ def info():
@app.route('/api/create_link_token_for_payment', methods=['POST'])
def create_link_token_for_payment():
global payment_id
- try:
- request = PaymentInitiationRecipientCreateRequest(
- name='John Doe',
- bacs=RecipientBACSNullable(account='26207729', sort_code='560029'),
- address=PaymentInitiationAddress(
- street=['street name 999'],
- city='city',
- postal_code='99999',
- country='GB'
- )
- )
- response = client.payment_initiation_recipient_create(
- request)
- recipient_id = response['recipient_id']
-
- request = PaymentInitiationPaymentCreateRequest(
- recipient_id=recipient_id,
- reference='TestPayment',
- amount=PaymentAmount(
- PaymentAmountCurrency('GBP'),
- value=100.00
- )
+ request = PaymentInitiationRecipientCreateRequest(
+ name='John Doe',
+ bacs=RecipientBACSNullable(account='26207729', sort_code='560029'),
+ address=PaymentInitiationAddress(
+ street=['street name 999'],
+ city='city',
+ postal_code='99999',
+ country='GB'
)
- response = client.payment_initiation_payment_create(
- request
+ )
+ response = client.payment_initiation_recipient_create(
+ request)
+ recipient_id = response['recipient_id']
+
+ request = PaymentInitiationPaymentCreateRequest(
+ recipient_id=recipient_id,
+ reference='TestPayment',
+ amount=PaymentAmount(
+ PaymentAmountCurrency('GBP'),
+ value=100.00
)
- pretty_print_response(response.to_dict())
-
- # We store the payment_id in memory for demo purposes - in production, store it in a secure
- # persistent data store along with the Payment metadata, such as userId.
- payment_id = response['payment_id']
-
- linkRequest = LinkTokenCreateRequest(
- # The 'payment_initiation' product has to be the only element in the 'products' list.
- products=[Products('payment_initiation')],
- client_name='Plaid Test',
- # Institutions from all listed countries will be shown.
- country_codes=list(map(lambda x: CountryCode(x), PLAID_COUNTRY_CODES)),
- language='en',
- user=LinkTokenCreateRequestUser(
- # This should correspond to a unique id for the current user.
- # Typically, this will be a user ID number from your application.
- # Personally identifiable information, such as an email address or phone number, should not be used here.
- client_user_id=str(time.time())
- ),
- payment_initiation=LinkTokenCreateRequestPaymentInitiation(
- payment_id=payment_id
- )
+ )
+ response = client.payment_initiation_payment_create(
+ request
+ )
+ pretty_print_response(response.to_dict())
+
+ # We store the payment_id in memory for demo purposes - in production, store it in a secure
+ # persistent data store along with the Payment metadata, such as userId.
+ payment_id = response['payment_id']
+
+ linkRequest = LinkTokenCreateRequest(
+ # The 'payment_initiation' product has to be the only element in the 'products' list.
+ products=[Products('payment_initiation')],
+ client_name='Plaid Test',
+ # Institutions from all listed countries will be shown.
+ country_codes=list(map(lambda x: CountryCode(x), PLAID_COUNTRY_CODES)),
+ language='en',
+ user=LinkTokenCreateRequestUser(
+ # This should correspond to a unique id for the current user.
+ # Typically, this will be a user ID number from your application.
+ # Personally identifiable information, such as an email address or phone number, should not be used here.
+ client_user_id=str(time.time())
+ ),
+ payment_initiation=LinkTokenCreateRequestPaymentInitiation(
+ payment_id=payment_id
)
+ )
- if PLAID_REDIRECT_URI!=None:
- linkRequest['redirect_uri']=PLAID_REDIRECT_URI
- linkResponse = client.link_token_create(linkRequest)
- pretty_print_response(linkResponse.to_dict())
- return jsonify(linkResponse.to_dict())
- except plaid.ApiException as e:
- return json.loads(e.body)
+ if PLAID_REDIRECT_URI!=None:
+ linkRequest['redirect_uri']=PLAID_REDIRECT_URI
+ linkResponse = client.link_token_create(linkRequest)
+ pretty_print_response(linkResponse.to_dict())
+ return jsonify(linkResponse.to_dict())
@app.route('/api/create_link_token', methods=['POST'])
def create_link_token():
global user_token
global user_id
- try:
- # Build request based on whether we have user_token or user_id
- cra_products = ["cra_base_report", "cra_income_insights", "cra_partner_insights"]
- is_cra = any(product in cra_products for product in PLAID_PRODUCTS)
-
- if is_cra and user_id and not user_token:
- # For user_id, don't include user field
- request = LinkTokenCreateRequest(
- products=products,
- client_name="Plaid Quickstart",
- country_codes=list(map(lambda x: CountryCode(x), PLAID_COUNTRY_CODES)),
- language='en'
- )
- else:
- # For user_token or non-CRA products, include user field
- request = LinkTokenCreateRequest(
- products=products,
- client_name="Plaid Quickstart",
- country_codes=list(map(lambda x: CountryCode(x), PLAID_COUNTRY_CODES)),
- language='en',
- user=LinkTokenCreateRequestUser(
- client_user_id=str(time.time())
- )
+ # Build request based on whether we have user_token or user_id
+ cra_products = ["cra_base_report", "cra_income_insights", "cra_partner_insights"]
+ is_cra = any(product in cra_products for product in PLAID_PRODUCTS)
+
+ if is_cra and user_id and not user_token:
+ # For user_id, don't include user field
+ request = LinkTokenCreateRequest(
+ products=products,
+ client_name="Plaid Quickstart",
+ country_codes=list(map(lambda x: CountryCode(x), PLAID_COUNTRY_CODES)),
+ language='en'
+ )
+ else:
+ # For user_token or non-CRA products, include user field
+ request = LinkTokenCreateRequest(
+ products=products,
+ client_name="Plaid Quickstart",
+ country_codes=list(map(lambda x: CountryCode(x), PLAID_COUNTRY_CODES)),
+ language='en',
+ user=LinkTokenCreateRequestUser(
+ client_user_id=str(time.time())
)
+ )
- if PLAID_REDIRECT_URI!=None:
- request['redirect_uri']=PLAID_REDIRECT_URI
- if Products('statements') in products:
- statements=LinkTokenCreateRequestStatements(
- end_date=date.today(),
- start_date=date.today()-timedelta(days=30)
- )
- request['statements']=statements
-
- if is_cra:
- # Use user_token if available, otherwise use user_id
- if user_token:
- request['user_token'] = user_token
- elif user_id:
- request['user_id'] = user_id
- request['consumer_report_permissible_purpose'] = ConsumerReportPermissiblePurpose('ACCOUNT_REVIEW_CREDIT')
- request['cra_options'] = LinkTokenCreateRequestCraOptions(
- days_requested=60
- )
+ if PLAID_REDIRECT_URI!=None:
+ request['redirect_uri']=PLAID_REDIRECT_URI
+ if Products('statements') in products:
+ statements=LinkTokenCreateRequestStatements(
+ end_date=date.today(),
+ start_date=date.today()-timedelta(days=30)
+ )
+ request['statements']=statements
+
+ if is_cra:
+ # Use user_token if available, otherwise use user_id
+ if user_token:
+ request['user_token'] = user_token
+ elif user_id:
+ request['user_id'] = user_id
+ request['consumer_report_permissible_purpose'] = ConsumerReportPermissiblePurpose('ACCOUNT_REVIEW_CREDIT')
+ request['cra_options'] = LinkTokenCreateRequestCraOptions(
+ days_requested=60
+ )
# create link token
- response = client.link_token_create(request)
- return jsonify(response.to_dict())
- except plaid.ApiException as e:
- print(e)
- return json.loads(e.body)
+ response = client.link_token_create(request)
+ return jsonify(response.to_dict())
# Create a user token which can be used for Plaid Check, Income, or Multi-Item link flows
# https://plaid.com/docs/api/users/#usercreate
@@ -342,11 +335,9 @@ def create_user_token():
if 'user_id' in user_response:
user_id = user_response['user_id']
return jsonify(user_response.to_dict())
- except plaid.ApiException as retry_error:
- print(retry_error)
- return jsonify(json.loads(retry_error.body)), retry_error.status
- print(e)
- return jsonify(json.loads(e.body)), e.status
+ except plaid.ApiException:
+ raise
+ raise
# Exchange token flow - exchange a Link public_token for
@@ -360,15 +351,12 @@ def get_access_token():
global item_id
global transfer_id
public_token = request.form['public_token']
- try:
- exchange_request = ItemPublicTokenExchangeRequest(
- public_token=public_token)
- exchange_response = client.item_public_token_exchange(exchange_request)
- access_token = exchange_response['access_token']
- item_id = exchange_response['item_id']
- return jsonify(exchange_response.to_dict())
- except plaid.ApiException as e:
- return json.loads(e.body)
+ exchange_request = ItemPublicTokenExchangeRequest(
+ public_token=public_token)
+ exchange_response = client.item_public_token_exchange(exchange_request)
+ access_token = exchange_response['access_token']
+ item_id = exchange_response['item_id']
+ return jsonify(exchange_response.to_dict())
# Retrieve ACH or ETF account numbers for an Item
@@ -377,16 +365,12 @@ def get_access_token():
@app.route('/api/auth', methods=['GET'])
def get_auth():
- try:
- request = AuthGetRequest(
- access_token=access_token
- )
- response = client.auth_get(request)
- pretty_print_response(response.to_dict())
- return jsonify(response.to_dict())
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ request = AuthGetRequest(
+ access_token=access_token
+ )
+ response = client.auth_get(request)
+ pretty_print_response(response.to_dict())
+ return jsonify(response.to_dict())
# Retrieve Transactions for an Item
@@ -403,39 +387,34 @@ def get_transactions():
modified = []
removed = [] # Removed transaction ids
has_more = True
- try:
- # Iterate through each page of new transaction updates for item
- while has_more:
- request = TransactionsSyncRequest(
- access_token=access_token,
- cursor=cursor,
- )
- response = client.transactions_sync(request).to_dict()
- cursor = response['next_cursor']
- # If no transactions are available yet, wait and poll the endpoint.
- # Normally, we would listen for a webhook, but the Quickstart doesn't
- # support webhooks. For a webhook example, see
- # https://github.com/plaid/tutorial-resources or
- # https://github.com/plaid/pattern
- if cursor == '':
- time.sleep(2)
- continue
- # If cursor is not an empty string, we got results,
- # so add this page of results
- added.extend(response['added'])
- modified.extend(response['modified'])
- removed.extend(response['removed'])
- has_more = response['has_more']
- pretty_print_response(response)
-
- # Return the 8 most recent transactions
- latest_transactions = sorted(added, key=lambda t: t['date'])[-8:]
- return jsonify({
- 'latest_transactions': latest_transactions})
-
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ # Iterate through each page of new transaction updates for item
+ while has_more:
+ request = TransactionsSyncRequest(
+ access_token=access_token,
+ cursor=cursor,
+ )
+ response = client.transactions_sync(request).to_dict()
+ cursor = response['next_cursor']
+ # If no transactions are available yet, wait and poll the endpoint.
+ # Normally, we would listen for a webhook, but the Quickstart doesn't
+ # support webhooks. For a webhook example, see
+ # https://github.com/plaid/tutorial-resources or
+ # https://github.com/plaid/pattern
+ if cursor == '':
+ time.sleep(2)
+ continue
+ # If cursor is not an empty string, we got results,
+ # so add this page of results
+ added.extend(response['added'])
+ modified.extend(response['modified'])
+ removed.extend(response['removed'])
+ has_more = response['has_more']
+ pretty_print_response(response)
+
+ # Return the 8 most recent transactions
+ latest_transactions = sorted(added, key=lambda t: t['date'])[-8:]
+ return jsonify({
+ 'latest_transactions': latest_transactions})
# Retrieve Identity data for an Item
@@ -444,17 +423,13 @@ def get_transactions():
@app.route('/api/identity', methods=['GET'])
def get_identity():
- try:
- request = IdentityGetRequest(
- access_token=access_token
- )
- response = client.identity_get(request)
- pretty_print_response(response.to_dict())
- return jsonify(
- {'error': None, 'identity': response.to_dict()['accounts']})
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ request = IdentityGetRequest(
+ access_token=access_token
+ )
+ response = client.identity_get(request)
+ pretty_print_response(response.to_dict())
+ return jsonify(
+ {'error': None, 'identity': response.to_dict()['accounts']})
# Retrieve real-time balance data for each of an Item's accounts
@@ -463,14 +438,10 @@ def get_identity():
@app.route('/api/balance', methods=['GET'])
def get_balance():
- try:
- balance_request = AccountsBalanceGetRequest(access_token=access_token)
- balance_response = client.accounts_balance_get(balance_request)
- pretty_print_response(balance_response.to_dict())
- return jsonify(balance_response.to_dict())
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ balance_request = AccountsBalanceGetRequest(access_token=access_token)
+ balance_response = client.accounts_balance_get(balance_request)
+ pretty_print_response(balance_response.to_dict())
+ return jsonify(balance_response.to_dict())
# Retrieve an Item's accounts
@@ -479,16 +450,12 @@ def get_balance():
@app.route('/api/accounts', methods=['GET'])
def get_accounts():
- try:
- request = AccountsGetRequest(
- access_token=access_token
- )
- response = client.accounts_get(request)
- pretty_print_response(response.to_dict())
- return jsonify(response.to_dict())
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ request = AccountsGetRequest(
+ access_token=access_token
+ )
+ response = client.accounts_get(request)
+ pretty_print_response(response.to_dict())
+ return jsonify(response.to_dict())
# Create and then retrieve an Asset Report for one or more Items. Note that an
@@ -499,48 +466,44 @@ def get_accounts():
@app.route('/api/assets', methods=['GET'])
def get_assets():
- try:
- request = AssetReportCreateRequest(
- access_tokens=[access_token],
- days_requested=60,
- options=AssetReportCreateRequestOptions(
- webhook='https://www.example.com',
- client_report_id='123',
- user=AssetReportUser(
- client_user_id='789',
- first_name='Jane',
- middle_name='Leah',
- last_name='Doe',
- ssn='123-45-6789',
- phone_number='(555) 123-4567',
- email='jane.doe@example.com',
- )
+ request = AssetReportCreateRequest(
+ access_tokens=[access_token],
+ days_requested=60,
+ options=AssetReportCreateRequestOptions(
+ webhook='https://www.example.com',
+ client_report_id='123',
+ user=AssetReportUser(
+ client_user_id='789',
+ first_name='Jane',
+ middle_name='Leah',
+ last_name='Doe',
+ ssn='123-45-6789',
+ phone_number='(555) 123-4567',
+ email='jane.doe@example.com',
)
)
-
- response = client.asset_report_create(request)
- pretty_print_response(response.to_dict())
- asset_report_token = response['asset_report_token']
-
- # Poll for the completion of the Asset Report.
- request = AssetReportGetRequest(
- asset_report_token=asset_report_token,
- )
- response = poll_with_retries(lambda: client.asset_report_get(request))
- asset_report_json = response['report']
-
- request = AssetReportPDFGetRequest(
- asset_report_token=asset_report_token,
- )
- pdf = client.asset_report_pdf_get(request)
- return jsonify({
- 'error': None,
- 'json': asset_report_json.to_dict(),
- 'pdf': base64.b64encode(pdf.read()).decode('utf-8'),
- })
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ )
+
+ response = client.asset_report_create(request)
+ pretty_print_response(response.to_dict())
+ asset_report_token = response['asset_report_token']
+
+ # Poll for the completion of the Asset Report.
+ request = AssetReportGetRequest(
+ asset_report_token=asset_report_token,
+ )
+ response = poll_with_retries(lambda: client.asset_report_get(request))
+ asset_report_json = response['report']
+
+ request = AssetReportPDFGetRequest(
+ asset_report_token=asset_report_token,
+ )
+ pdf = client.asset_report_pdf_get(request)
+ return jsonify({
+ 'error': None,
+ 'json': asset_report_json.to_dict(),
+ 'pdf': base64.b64encode(pdf.read()).decode('utf-8'),
+ })
# Retrieve investment holdings data for an Item
@@ -549,14 +512,10 @@ def get_assets():
@app.route('/api/holdings', methods=['GET'])
def get_holdings():
- try:
- request = InvestmentsHoldingsGetRequest(access_token=access_token)
- response = client.investments_holdings_get(request)
- pretty_print_response(response.to_dict())
- return jsonify({'error': None, 'holdings': response.to_dict()})
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ request = InvestmentsHoldingsGetRequest(access_token=access_token)
+ response = client.investments_holdings_get(request)
+ pretty_print_response(response.to_dict())
+ return jsonify({'error': None, 'holdings': response.to_dict()})
# Retrieve Investment Transactions for an Item
@@ -569,23 +528,18 @@ def get_investments_transactions():
start_date = (dt.datetime.now() - dt.timedelta(days=(30)))
end_date = dt.datetime.now()
- try:
- options = InvestmentsTransactionsGetRequestOptions()
- request = InvestmentsTransactionsGetRequest(
- access_token=access_token,
- start_date=start_date.date(),
- end_date=end_date.date(),
- options=options
- )
- response = client.investments_transactions_get(
- request)
- pretty_print_response(response.to_dict())
- return jsonify(
- {'error': None, 'investments_transactions': response.to_dict()})
-
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ options = InvestmentsTransactionsGetRequestOptions()
+ request = InvestmentsTransactionsGetRequest(
+ access_token=access_token,
+ start_date=start_date.date(),
+ end_date=end_date.date(),
+ options=options
+ )
+ response = client.investments_transactions_get(
+ request)
+ pretty_print_response(response.to_dict())
+ return jsonify(
+ {'error': None, 'investments_transactions': response.to_dict()})
# This functionality is only relevant for the ACH Transfer product.
# Authorize a transfer
@@ -597,74 +551,58 @@ def transfer_authorization():
request = AccountsGetRequest(access_token=access_token)
response = client.accounts_get(request)
account_id = response['accounts'][0]['account_id']
- try:
- request = TransferAuthorizationCreateRequest(
- access_token=access_token,
- account_id=account_id,
- type=TransferType('debit'),
- network=TransferNetwork('ach'),
- amount='1.00',
- ach_class=ACHClass('ppd'),
- user=TransferAuthorizationUserInRequest(
- legal_name='FirstName LastName',
- email_address='foobar@email.com',
- address=TransferUserAddressInRequest(
- street='123 Main St.',
- city='San Francisco',
- region='CA',
- postal_code='94053',
- country='US'
- ),
+ request = TransferAuthorizationCreateRequest(
+ access_token=access_token,
+ account_id=account_id,
+ type=TransferType('debit'),
+ network=TransferNetwork('ach'),
+ amount='1.00',
+ ach_class=ACHClass('ppd'),
+ user=TransferAuthorizationUserInRequest(
+ legal_name='FirstName LastName',
+ email_address='foobar@email.com',
+ address=TransferUserAddressInRequest(
+ street='123 Main St.',
+ city='San Francisco',
+ region='CA',
+ postal_code='94053',
+ country='US'
),
- )
- response = client.transfer_authorization_create(request)
- pretty_print_response(response.to_dict())
- authorization_id = response['authorization']['id']
- return jsonify(response.to_dict())
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ ),
+ )
+ response = client.transfer_authorization_create(request)
+ pretty_print_response(response.to_dict())
+ authorization_id = response['authorization']['id']
+ return jsonify(response.to_dict())
# Create Transfer for a specified Transfer ID
@app.route('/api/transfer_create', methods=['GET'])
def transfer():
- try:
- request = TransferCreateRequest(
- access_token=access_token,
- account_id=account_id,
- authorization_id=authorization_id,
- description='Debit')
- response = client.transfer_create(request)
- pretty_print_response(response.to_dict())
- return jsonify(response.to_dict())
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ request = TransferCreateRequest(
+ access_token=access_token,
+ account_id=account_id,
+ authorization_id=authorization_id,
+ description='Debit')
+ response = client.transfer_create(request)
+ pretty_print_response(response.to_dict())
+ return jsonify(response.to_dict())
@app.route('/api/statements', methods=['GET'])
def statements():
- try:
- request = StatementsListRequest(access_token=access_token)
- response = client.statements_list(request)
- pretty_print_response(response.to_dict())
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
- try:
- request = StatementsDownloadRequest(
- access_token=access_token,
- statement_id=response['accounts'][0]['statements'][0]['statement_id']
- )
- pdf = client.statements_download(request)
- return jsonify({
- 'error': None,
- 'json': response.to_dict(),
- 'pdf': base64.b64encode(pdf.read()).decode('utf-8'),
- })
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ request = StatementsListRequest(access_token=access_token)
+ response = client.statements_list(request)
+ pretty_print_response(response.to_dict())
+ request = StatementsDownloadRequest(
+ access_token=access_token,
+ statement_id=response['accounts'][0]['statements'][0]['statement_id']
+ )
+ pdf = client.statements_download(request)
+ return jsonify({
+ 'error': None,
+ 'json': response.to_dict(),
+ 'pdf': base64.b64encode(pdf.read()).decode('utf-8'),
+ })
@@ -675,27 +613,23 @@ def signal():
request = AccountsGetRequest(access_token=access_token)
response = client.accounts_get(request)
account_id = response['accounts'][0]['account_id']
- try:
- # Generate unique transaction ID using timestamp and random component
- client_transaction_id = f"txn-{int(time.time() * 1000)}-{uuid.uuid4().hex[:8]}"
-
- signal_request_params = {
- 'access_token': access_token,
- 'account_id': account_id,
- 'client_transaction_id': client_transaction_id,
- 'amount': 100.00
- }
-
- if SIGNAL_RULESET_KEY:
- signal_request_params['ruleset_key'] = SIGNAL_RULESET_KEY
-
- request = SignalEvaluateRequest(**signal_request_params)
- response = client.signal_evaluate(request)
- pretty_print_response(response.to_dict())
- return jsonify(response.to_dict())
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ # Generate unique transaction ID using timestamp and random component
+ client_transaction_id = f"txn-{int(time.time() * 1000)}-{uuid.uuid4().hex[:8]}"
+
+ signal_request_params = {
+ 'access_token': access_token,
+ 'account_id': account_id,
+ 'client_transaction_id': client_transaction_id,
+ 'amount': 100.00
+ }
+
+ if SIGNAL_RULESET_KEY:
+ signal_request_params['ruleset_key'] = SIGNAL_RULESET_KEY
+
+ request = SignalEvaluateRequest(**signal_request_params)
+ response = client.signal_evaluate(request)
+ pretty_print_response(response.to_dict())
+ return jsonify(response.to_dict())
# This functionality is only relevant for the UK Payment Initiation product.
@@ -705,14 +639,10 @@ def signal():
@app.route('/api/payment', methods=['GET'])
def payment():
global payment_id
- try:
- request = PaymentInitiationPaymentGetRequest(payment_id=payment_id)
- response = client.payment_initiation_payment_get(request)
- pretty_print_response(response.to_dict())
- return jsonify({'error': None, 'payment': response.to_dict()})
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ request = PaymentInitiationPaymentGetRequest(payment_id=payment_id)
+ response = client.payment_initiation_payment_get(request)
+ pretty_print_response(response.to_dict())
+ return jsonify({'error': None, 'payment': response.to_dict()})
# Retrieve high-level information about an Item
@@ -721,102 +651,86 @@ def payment():
@app.route('/api/item', methods=['GET'])
def item():
- try:
- request = ItemGetRequest(access_token=access_token)
- response = client.item_get(request)
- request = InstitutionsGetByIdRequest(
- institution_id=response['item']['institution_id'],
- country_codes=list(map(lambda x: CountryCode(x), PLAID_COUNTRY_CODES))
- )
- institution_response = client.institutions_get_by_id(request)
- pretty_print_response(response.to_dict())
- pretty_print_response(institution_response.to_dict())
- return jsonify({'error': None, 'item': response.to_dict()[
- 'item'], 'institution': institution_response.to_dict()['institution']})
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ request = ItemGetRequest(access_token=access_token)
+ response = client.item_get(request)
+ request = InstitutionsGetByIdRequest(
+ institution_id=response['item']['institution_id'],
+ country_codes=list(map(lambda x: CountryCode(x), PLAID_COUNTRY_CODES))
+ )
+ institution_response = client.institutions_get_by_id(request)
+ pretty_print_response(response.to_dict())
+ pretty_print_response(institution_response.to_dict())
+ return jsonify({'error': None, 'item': response.to_dict()[
+ 'item'], 'institution': institution_response.to_dict()['institution']})
# Retrieve CRA Base Report and PDF
# Base report: https://plaid.com/docs/check/api/#cracheck_reportbase_reportget
# PDF: https://plaid.com/docs/check/api/#cracheck_reportpdfget
@app.route('/api/cra/get_base_report', methods=['GET'])
def cra_check_report():
- try:
- # Use user_token if available, otherwise use user_id
- if user_token:
- base_report_request = CraCheckReportBaseReportGetRequest(user_token=user_token, item_ids=[])
- elif user_id:
- base_report_request = CraCheckReportBaseReportGetRequest(user_id=user_id, item_ids=[])
+ # Use user_token if available, otherwise use user_id
+ if user_token:
+ base_report_request = CraCheckReportBaseReportGetRequest(user_token=user_token, item_ids=[])
+ elif user_id:
+ base_report_request = CraCheckReportBaseReportGetRequest(user_id=user_id, item_ids=[])
- get_response = poll_with_retries(lambda: client.cra_check_report_base_report_get(base_report_request))
- pretty_print_response(get_response.to_dict())
+ get_response = poll_with_retries(lambda: client.cra_check_report_base_report_get(base_report_request))
+ pretty_print_response(get_response.to_dict())
- if user_token:
- pdf_request = CraCheckReportPDFGetRequest(user_token=user_token)
- elif user_id:
- pdf_request = CraCheckReportPDFGetRequest(user_id=user_id)
+ if user_token:
+ pdf_request = CraCheckReportPDFGetRequest(user_token=user_token)
+ elif user_id:
+ pdf_request = CraCheckReportPDFGetRequest(user_id=user_id)
- pdf_response = client.cra_check_report_pdf_get(pdf_request)
+ pdf_response = client.cra_check_report_pdf_get(pdf_request)
- return jsonify({
- 'report': get_response.to_dict()['report'],
- 'pdf': base64.b64encode(pdf_response.read()).decode('utf-8')
- })
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ return jsonify({
+ 'report': get_response.to_dict()['report'],
+ 'pdf': base64.b64encode(pdf_response.read()).decode('utf-8')
+ })
# Retrieve CRA Income Insights and PDF with Insights
# Income insights: https://plaid.com/docs/check/api/#cracheck_reportincome_insightsget
# PDF w/ income insights: https://plaid.com/docs/check/api/#cracheck_reportpdfget
@app.route('/api/cra/get_income_insights', methods=['GET'])
def cra_income_insights():
- try:
- # Use user_token if available, otherwise use user_id
- insights_request = {}
- if user_token:
- insights_request = CraCheckReportIncomeInsightsGetRequest(user_token=user_token)
- elif user_id:
- insights_request = CraCheckReportIncomeInsightsGetRequest(user_id=user_id)
+ # Use user_token if available, otherwise use user_id
+ insights_request = {}
+ if user_token:
+ insights_request = CraCheckReportIncomeInsightsGetRequest(user_token=user_token)
+ elif user_id:
+ insights_request = CraCheckReportIncomeInsightsGetRequest(user_id=user_id)
- get_response = poll_with_retries(lambda: client.cra_check_report_income_insights_get(insights_request))
- pretty_print_response(get_response.to_dict())
+ get_response = poll_with_retries(lambda: client.cra_check_report_income_insights_get(insights_request))
+ pretty_print_response(get_response.to_dict())
- pdf_request = {}
- if user_token:
- pdf_request = CraCheckReportPDFGetRequest(user_token=user_token, add_ons=[CraPDFAddOns('cra_income_insights')])
- elif user_id:
- pdf_request = CraCheckReportPDFGetRequest(user_id=user_id, add_ons=[CraPDFAddOns('cra_income_insights')])
+ pdf_request = {}
+ if user_token:
+ pdf_request = CraCheckReportPDFGetRequest(user_token=user_token, add_ons=[CraPDFAddOns('cra_income_insights')])
+ elif user_id:
+ pdf_request = CraCheckReportPDFGetRequest(user_id=user_id, add_ons=[CraPDFAddOns('cra_income_insights')])
- pdf_response = client.cra_check_report_pdf_get(pdf_request)
+ pdf_response = client.cra_check_report_pdf_get(pdf_request)
- return jsonify({
- 'report': get_response.to_dict()['report'],
- 'pdf': base64.b64encode(pdf_response.read()).decode('utf-8')
- })
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ return jsonify({
+ 'report': get_response.to_dict()['report'],
+ 'pdf': base64.b64encode(pdf_response.read()).decode('utf-8')
+ })
# Retrieve CRA Partner Insights
# https://plaid.com/docs/check/api/#cracheck_reportpartner_insightsget
@app.route('/api/cra/get_partner_insights', methods=['GET'])
def cra_partner_insights():
- try:
- # Use user_token if available, otherwise use user_id
- if user_token:
- partner_request = CraCheckReportPartnerInsightsGetRequest(user_token=user_token)
- elif user_id:
- partner_request = CraCheckReportPartnerInsightsGetRequest(user_id=user_id)
+ # Use user_token if available, otherwise use user_id
+ if user_token:
+ partner_request = CraCheckReportPartnerInsightsGetRequest(user_token=user_token)
+ elif user_id:
+ partner_request = CraCheckReportPartnerInsightsGetRequest(user_id=user_id)
- response = poll_with_retries(lambda: client.cra_check_report_partner_insights_get(partner_request))
- pretty_print_response(response.to_dict())
+ response = poll_with_retries(lambda: client.cra_check_report_partner_insights_get(partner_request))
+ pretty_print_response(response.to_dict())
- return jsonify(response.to_dict())
- except plaid.ApiException as e:
- error_response = format_error(e)
- return jsonify(error_response)
+ return jsonify(response.to_dict())
# Since this quickstart does not support webhooks, this function can be used to poll
# an API that would otherwise be triggered by a webhook.
@@ -828,8 +742,13 @@ def poll_with_retries(request_callback, ms=1000, retries_left=20):
try:
return request_callback()
except plaid.ApiException as e:
- response = json.loads(e.body)
- if response['error_code'] != 'PRODUCT_NOT_READY':
+ try:
+ response = json.loads(e.body)
+ error_code = response.get('error_code', '')
+ except (json.JSONDecodeError, TypeError):
+ error_code = ''
+ is_retryable = error_code == 'PRODUCT_NOT_READY' or e.status >= 500
+ if not is_retryable:
raise e
elif retries_left == 0:
raise Exception('Ran out of retries while polling') from e
@@ -842,8 +761,21 @@ def pretty_print_response(response):
def format_error(e):
response = json.loads(e.body)
- return {'error': {'status_code': e.status, 'display_message':
- response['error_message'], 'error_code': response['error_code'], 'error_type': response['error_type']}}
+ return {'error': {**response, 'status_code': e.status}}
+
+@app.route('/api/link_exit_error', methods=['POST'])
+def link_exit_error():
+ data = request.get_json()
+ print('[Link Exit Error (frontend)]')
+ pretty_print_response(data)
+ return jsonify({'status': 'logged'})
+
+
+@app.errorhandler(plaid.ApiException)
+def handle_plaid_error(e):
+ response = format_error(e)
+ pretty_print_response(response)
+ return jsonify(response), e.status
if __name__ == '__main__':
app.run(port=int(os.getenv('PORT', 8000)))
diff --git a/ruby/app.rb b/ruby/app.rb
index 0fd8b3875..00acfa2a6 100644
--- a/ruby/app.rb
+++ b/ruby/app.rb
@@ -10,6 +10,8 @@
require 'sinatra'
set :port, ENV['APP_PORT'] || 8000
+set :show_exceptions, false
+set :dump_errors, false
# disable CSRF warning and Rack protection on localhost due to usage of local /api proxy in react app.
# delete this for a production application.
@@ -76,162 +78,113 @@
# Retrieve Transactions for an Item
# https://plaid.com/docs/#transactions
get '/api/transactions' do
- begin
- # Set cursor to empty to receive all historical updates
- cursor = ''
-
- # New transaction updates since "cursor"
- added = []
- modified = []
- removed = [] # Removed transaction ids
- has_more = true
- # Iterate through each page of new transaction updates for item
- while has_more
- request = Plaid::TransactionsSyncRequest.new(
- {
- access_token: access_token,
- cursor: cursor
- }
- )
- response = client.transactions_sync(request)
- cursor = response.next_cursor
-
- # If no transactions are available yet, wait and poll the endpoint.
- # Normally, we would listen for a webhook but the Quickstart doesn't
- # support webhooks. For a webhook example, see
- # https://github.com/plaid/tutorial-resources or
- # https://github.com/plaid/pattern
- if cursor == ""
- sleep 2
- next
- end
-
- # Add this page of results
- added += response.added
- modified += response.modified
- removed += response.removed
- has_more = response.has_more
- pretty_print_response(response.to_hash)
+ # Set cursor to empty to receive all historical updates
+ cursor = ''
+
+ # New transaction updates since "cursor"
+ added = []
+ modified = []
+ removed = [] # Removed transaction ids
+ has_more = true
+ # Iterate through each page of new transaction updates for item
+ while has_more
+ request = Plaid::TransactionsSyncRequest.new(
+ {
+ access_token: access_token,
+ cursor: cursor
+ }
+ )
+ response = client.transactions_sync(request)
+ cursor = response.next_cursor
+
+ # If no transactions are available yet, wait and poll the endpoint.
+ # Normally, we would listen for a webhook but the Quickstart doesn't
+ # support webhooks. For a webhook example, see
+ # https://github.com/plaid/tutorial-resources or
+ # https://github.com/plaid/pattern
+ if cursor == ""
+ sleep 2
+ next
end
- # Return the 8 most recent transactions
- content_type :json
- { latest_transactions: added.sort_by(&:date).last(8).map(&:to_hash) }.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
+
+ # Add this page of results
+ added += response.added
+ modified += response.modified
+ removed += response.removed
+ has_more = response.has_more
+ pretty_print_response(response.to_hash)
end
+ # Return the 8 most recent transactions
+ content_type :json
+ { latest_transactions: added.sort_by(&:date).last(8).map(&:to_hash) }.to_json
end
# Retrieve ACH or ETF account numbers for an Item
# https://plaid.com/docs/#auth
get '/api/auth' do
- begin
- auth_get_request = Plaid::AuthGetRequest.new({ access_token: access_token })
- auth_response = client.auth_get(auth_get_request)
- pretty_print_response(auth_response.to_hash)
- content_type :json
- auth_response.to_hash.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ auth_get_request = Plaid::AuthGetRequest.new({ access_token: access_token })
+ auth_response = client.auth_get(auth_get_request)
+ pretty_print_response(auth_response.to_hash)
+ content_type :json
+ auth_response.to_hash.to_json
end
# Retrieve Identity data for an Item
# https://plaid.com/docs/#identity
get '/api/identity' do
- begin
- identity_get_request = Plaid::IdentityGetRequest.new({ access_token: access_token })
- identity_response = client.identity_get(identity_get_request)
- pretty_print_response(identity_response.to_hash)
- content_type :json
- { identity: identity_response.to_hash[:accounts] }.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ identity_get_request = Plaid::IdentityGetRequest.new({ access_token: access_token })
+ identity_response = client.identity_get(identity_get_request)
+ pretty_print_response(identity_response.to_hash)
+ content_type :json
+ { identity: identity_response.to_hash[:accounts] }.to_json
end
# Retrieve real-time balance data for each of an Item's accounts
# https://plaid.com/docs/#balance
get '/api/balance' do
- begin
- balance_request = Plaid::AccountsBalanceGetRequest.new({ access_token: access_token })
- balance_response = client.accounts_balance_get(balance_request)
- pretty_print_response(balance_response.to_hash)
- content_type :json
- balance_response.to_hash.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ balance_request = Plaid::AccountsBalanceGetRequest.new({ access_token: access_token })
+ balance_response = client.accounts_balance_get(balance_request)
+ pretty_print_response(balance_response.to_hash)
+ content_type :json
+ balance_response.to_hash.to_json
end
# Retrieve an Item's accounts
# https://plaid.com/docs/#accounts
get '/api/accounts' do
- begin
- accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token })
- account_response = client.accounts_get(accounts_get_request)
- pretty_print_response(account_response.to_hash)
- content_type :json
- account_response.to_hash.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token })
+ account_response = client.accounts_get(accounts_get_request)
+ pretty_print_response(account_response.to_hash)
+ content_type :json
+ account_response.to_hash.to_json
end
# Retrieve Holdings data for an Item
# https://plaid.com/docs/#investments
get '/api/holdings' do
- begin
- investments_holdings_get_request = Plaid::InvestmentsHoldingsGetRequest.new({ access_token: access_token })
- product_response = client.investments_holdings_get(investments_holdings_get_request)
- pretty_print_response(product_response.to_hash)
- content_type :json
- { holdings: product_response.to_hash }.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_hash.to_json
- end
+ investments_holdings_get_request = Plaid::InvestmentsHoldingsGetRequest.new({ access_token: access_token })
+ product_response = client.investments_holdings_get(investments_holdings_get_request)
+ pretty_print_response(product_response.to_hash)
+ content_type :json
+ { holdings: product_response.to_hash }.to_json
end
# Retrieve Investment Transactions for an Item
# https://plaid.com/docs/#investments
get '/api/investments_transactions' do
- begin
- start_date = (Date.today - 30)
- end_date = Date.today
- investments_transactions_get_request = Plaid::InvestmentsTransactionsGetRequest.new(
- {
- access_token: access_token,
- start_date: start_date,
- end_date: end_date
- }
- )
- transactions_response = client.investments_transactions_get(investments_transactions_get_request)
- pretty_print_response(transactions_response.to_hash)
- content_type :json
- { investments_transactions: transactions_response.to_hash }.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ start_date = (Date.today - 30)
+ end_date = Date.today
+ investments_transactions_get_request = Plaid::InvestmentsTransactionsGetRequest.new(
+ {
+ access_token: access_token,
+ start_date: start_date,
+ end_date: end_date
+ }
+ )
+ transactions_response = client.investments_transactions_get(investments_transactions_get_request)
+ pretty_print_response(transactions_response.to_hash)
+ content_type :json
+ { investments_transactions: transactions_response.to_hash }.to_json
end
# Create and then retrieve an Asset Report for one or more Items. Note that an
@@ -240,36 +193,29 @@
# https://plaid.com/docs/#assets
# rubocop:disable Metrics/BlockLength
get '/api/assets' do
- begin
- options = {
- client_report_id: '123',
- webhook: 'https://www.example.com',
- user: {
- client_user_id: '789',
- first_name: 'Jane',
- middle_name: 'Leah',
- last_name: 'Doe',
- ssn: '123-45-6789',
- phone_number: '(555) 123-4567',
- email: 'jane.doe@example.com'
- }
+ options = {
+ client_report_id: '123',
+ webhook: 'https://www.example.com',
+ user: {
+ client_user_id: '789',
+ first_name: 'Jane',
+ middle_name: 'Leah',
+ last_name: 'Doe',
+ ssn: '123-45-6789',
+ phone_number: '(555) 123-4567',
+ email: 'jane.doe@example.com'
}
- asset_report_create_request = Plaid::AssetReportCreateRequest.new(
- {
- access_tokens: [access_token],
- days_requested: 20,
- options: options
- }
- )
- asset_report_create_response =
- client.asset_report_create(asset_report_create_request)
- pretty_print_response(asset_report_create_response.to_hash)
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ }
+ asset_report_create_request = Plaid::AssetReportCreateRequest.new(
+ {
+ access_tokens: [access_token],
+ days_requested: 20,
+ options: options
+ }
+ )
+ asset_report_create_response =
+ client.asset_report_create(asset_report_create_request)
+ pretty_print_response(asset_report_create_response.to_hash)
asset_report_token = asset_report_create_response.asset_report_token
asset_report_json = nil
@@ -287,10 +233,7 @@
sleep(1)
next
end
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- return error_response.to_json
+ raise
end
end
@@ -313,21 +256,15 @@
end
get '/api/statements' do
- begin
- statements_list_request = Plaid::StatementsListRequest.new(
- {
- access_token: access_token
- }
- )
- statements_list_response =
- client.statements_list(statements_list_request)
- pretty_print_response(statements_list_response.to_hash)
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ statements_list_request = Plaid::StatementsListRequest.new(
+ {
+ access_token: access_token
+ }
+ )
+ statements_list_response =
+ client.statements_list(statements_list_request)
+ pretty_print_response(statements_list_response.to_hash)
+
statement_id = statements_list_response.accounts[0].statements[0].statement_id
statements_download_request = Plaid::StatementsDownloadRequest.new({ access_token: access_token, statement_id: statement_id })
statement_pdf = client.statements_download(statements_download_request)
@@ -342,275 +279,226 @@
# Retrieve high-level information about an Item
# https://plaid.com/docs/#retrieve-item
get '/api/item' do
- begin
- item_get_request = Plaid::ItemGetRequest.new({ access_token: access_token})
- item_response = client.item_get(item_get_request)
- institutions_get_by_id_request = Plaid::InstitutionsGetByIdRequest.new(
- {
- institution_id: item_response.item.institution_id,
- country_codes: ['US']
- }
- )
- institution_response =
- client.institutions_get_by_id(institutions_get_by_id_request)
- pretty_print_response(item_response.to_hash)
- pretty_print_response(institution_response.to_hash)
- content_type :json
- { item: item_response.item.to_hash,
- institution: institution_response.institution.to_hash }.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ item_get_request = Plaid::ItemGetRequest.new({ access_token: access_token})
+ item_response = client.item_get(item_get_request)
+ institutions_get_by_id_request = Plaid::InstitutionsGetByIdRequest.new(
+ {
+ institution_id: item_response.item.institution_id,
+ country_codes: ['US']
+ }
+ )
+ institution_response =
+ client.institutions_get_by_id(institutions_get_by_id_request)
+ pretty_print_response(item_response.to_hash)
+ pretty_print_response(institution_response.to_hash)
+ content_type :json
+ { item: item_response.item.to_hash,
+ institution: institution_response.institution.to_hash }.to_json
end
# This functionality is only relevant for the ACH Transfer product.
# Retrieve Transfer for a specified Transfer ID
get '/api/transfer_authorize' do
- begin
- # We call /accounts/get to obtain first account_id - in production,
- # account_id's should be persisted in a data store and retrieved
- # from there.
- accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token })
- accounts_get_response = client.accounts_get(accounts_get_request)
- account_id = accounts_get_response.accounts[0].account_id
-
- transfer_authorization_create_request = Plaid::TransferAuthorizationCreateRequest.new({
- access_token: access_token,
- account_id: account_id,
- type: 'debit',
- network: 'ach',
- amount: '1.00',
- ach_class: 'ppd',
- user: {
- legal_name: 'FirstName LastName',
- email_address: 'foobar@email.com',
- address: {
- street: '123 Main St.',
- city: 'San Francisco',
- region: 'CA',
- postal_code: '94053',
- country: 'US'
- }
- },
- })
- transfer_authorization_create_response = client.transfer_authorization_create(transfer_authorization_create_request)
- pretty_print_response(transfer_authorization_create_response.to_hash)
- authorization_id = transfer_authorization_create_response.authorization.id
- content_type :json
- transfer_authorization_create_response.to_hash.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ # We call /accounts/get to obtain first account_id - in production,
+ # account_id's should be persisted in a data store and retrieved
+ # from there.
+ accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token })
+ accounts_get_response = client.accounts_get(accounts_get_request)
+ account_id = accounts_get_response.accounts[0].account_id
+
+ transfer_authorization_create_request = Plaid::TransferAuthorizationCreateRequest.new({
+ access_token: access_token,
+ account_id: account_id,
+ type: 'debit',
+ network: 'ach',
+ amount: '1.00',
+ ach_class: 'ppd',
+ user: {
+ legal_name: 'FirstName LastName',
+ email_address: 'foobar@email.com',
+ address: {
+ street: '123 Main St.',
+ city: 'San Francisco',
+ region: 'CA',
+ postal_code: '94053',
+ country: 'US'
+ }
+ },
+ })
+ transfer_authorization_create_response = client.transfer_authorization_create(transfer_authorization_create_request)
+ pretty_print_response(transfer_authorization_create_response.to_hash)
+ authorization_id = transfer_authorization_create_response.authorization.id
+ content_type :json
+ transfer_authorization_create_response.to_hash.to_json
end
get '/api/signal_evaluate' do
- begin
- # We call /accounts/get to obtain first account_id - in production,
- # account_id's should be persisted in a data store and retrieved
- # from there.
- accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token })
- accounts_get_response = client.accounts_get(accounts_get_request)
- account_id = accounts_get_response.accounts[0].account_id
+ # We call /accounts/get to obtain first account_id - in production,
+ # account_id's should be persisted in a data store and retrieved
+ # from there.
+ accounts_get_request = Plaid::AccountsGetRequest.new({ access_token: access_token })
+ accounts_get_response = client.accounts_get(accounts_get_request)
+ account_id = accounts_get_response.accounts[0].account_id
- # Generate unique transaction ID using timestamp and random component
- client_transaction_id = "txn-#{Time.now.to_i}-#{SecureRandom.hex(4)}"
+ # Generate unique transaction ID using timestamp and random component
+ client_transaction_id = "txn-#{Time.now.to_i}-#{SecureRandom.hex(4)}"
- signal_request_params = {
- access_token: access_token,
- account_id: account_id,
- client_transaction_id: client_transaction_id,
- amount: 100.00
- }
-
- if ENV['SIGNAL_RULESET_KEY'] && !ENV['SIGNAL_RULESET_KEY'].empty?
- signal_request_params[:ruleset_key] = ENV['SIGNAL_RULESET_KEY']
- end
+ signal_request_params = {
+ access_token: access_token,
+ account_id: account_id,
+ client_transaction_id: client_transaction_id,
+ amount: 100.00
+ }
- signal_evaluate_request = Plaid::SignalEvaluateRequest.new(signal_request_params)
- signal_evaluate_response = client.signal_evaluate(signal_evaluate_request)
- pretty_print_response(signal_evaluate_response.to_hash)
- content_type :json
- signal_evaluate_response.to_hash.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
+ if ENV['SIGNAL_RULESET_KEY'] && !ENV['SIGNAL_RULESET_KEY'].empty?
+ signal_request_params[:ruleset_key] = ENV['SIGNAL_RULESET_KEY']
end
+
+ signal_evaluate_request = Plaid::SignalEvaluateRequest.new(signal_request_params)
+ signal_evaluate_response = client.signal_evaluate(signal_evaluate_request)
+ pretty_print_response(signal_evaluate_response.to_hash)
+ content_type :json
+ signal_evaluate_response.to_hash.to_json
end
get '/api/transfer_create' do
- begin
- transfer_create_request = Plaid::TransferCreateRequest.new({
- access_token: access_token,
- account_id: account_id,
- authorization_id: authorization_id,
- description: 'Debit'
- })
- transfer_create_response = client.transfer_create(transfer_create_request)
- pretty_print_response(transfer_create_response.to_hash)
- content_type :json
- transfer_create_response.to_hash.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ transfer_create_request = Plaid::TransferCreateRequest.new({
+ access_token: access_token,
+ account_id: account_id,
+ authorization_id: authorization_id,
+ description: 'Debit'
+ })
+ transfer_create_response = client.transfer_create(transfer_create_request)
+ pretty_print_response(transfer_create_response.to_hash)
+ content_type :json
+ transfer_create_response.to_hash.to_json
end
# This functionality is only relevant for the UK Payment Initiation product.
# Retrieve Payment for a specified Payment ID
get '/api/payment' do
- begin
- payment_initiation_payment_get_request = Plaid::PaymentInitiationPaymentGetRequest.new({ payment_id: payment_id})
- payment_get_response = client.payment_initiation_payment_get(payment_initiation_payment_get_request)
- pretty_print_response(payment_get_response.to_hash)
- content_type :json
- { payment: payment_get_response.to_hash}.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ payment_initiation_payment_get_request = Plaid::PaymentInitiationPaymentGetRequest.new({ payment_id: payment_id})
+ payment_get_response = client.payment_initiation_payment_get(payment_initiation_payment_get_request)
+ pretty_print_response(payment_get_response.to_hash)
+ content_type :json
+ { payment: payment_get_response.to_hash}.to_json
end
post '/api/create_link_token' do
- begin
- link_token_create_request = Plaid::LinkTokenCreateRequest.new(
- {
- user: { client_user_id: 'user-id' },
- client_name: 'Plaid Quickstart',
- products: ENV['PLAID_PRODUCTS'].split(','),
- country_codes: ENV['PLAID_COUNTRY_CODES'].split(','),
- language: 'en',
- redirect_uri: nil_if_empty_envvar('PLAID_REDIRECT_URI')
- }
+ link_token_create_request = Plaid::LinkTokenCreateRequest.new(
+ {
+ user: { client_user_id: 'user-id' },
+ client_name: 'Plaid Quickstart',
+ products: ENV['PLAID_PRODUCTS'].split(','),
+ country_codes: ENV['PLAID_COUNTRY_CODES'].split(','),
+ language: 'en',
+ redirect_uri: nil_if_empty_envvar('PLAID_REDIRECT_URI')
+ }
+ )
+ if ENV['PLAID_PRODUCTS'].split(',').include?("statements")
+ today = Date.today
+ statements = Plaid::LinkTokenCreateRequestStatements.new(
+ end_date: today,
+ start_date: today-30
)
- if ENV['PLAID_PRODUCTS'].split(',').include?("statements")
- today = Date.today
- statements = Plaid::LinkTokenCreateRequestStatements.new(
- end_date: today,
- start_date: today-30
- )
- link_token_create_request.statements=statements
- end
- if products.any? { |product| product.start_with?("cra_") }
- link_token_create_request.cra_options = Plaid::LinkTokenCreateRequestCraOptions.new(
- days_requested: 60
- )
- # Use user_token if available, otherwise use user_id
- if user_token
- link_token_create_request.user_token = user_token
- # Keep user object when using user_token
- elsif user_id
- link_token_create_request.user_id = user_id
- # Remove user object when using user_id
- link_token_create_request.user = nil
- end
- link_token_create_request.consumer_report_permissible_purpose = Plaid::ConsumerReportPermissiblePurpose::ACCOUNT_REVIEW_CREDIT
+ link_token_create_request.statements=statements
+ end
+ if products.any? { |product| product.start_with?("cra_") }
+ link_token_create_request.cra_options = Plaid::LinkTokenCreateRequestCraOptions.new(
+ days_requested: 60
+ )
+ # Use user_token if available, otherwise use user_id
+ if user_token
+ link_token_create_request.user_token = user_token
+ # Keep user object when using user_token
+ elsif user_id
+ link_token_create_request.user_id = user_id
+ # Remove user object when using user_id
+ link_token_create_request.user = nil
end
- link_response = client.link_token_create(link_token_create_request)
- pretty_print_response(link_response.to_hash)
- content_type :json
- { link_token: link_response.link_token }.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
+ link_token_create_request.consumer_report_permissible_purpose = Plaid::ConsumerReportPermissiblePurpose::ACCOUNT_REVIEW_CREDIT
end
+ link_response = client.link_token_create(link_token_create_request)
+ pretty_print_response(link_response.to_hash)
+ content_type :json
+ { link_token: link_response.link_token }.to_json
end
# Create a user token which can be used for Plaid Check, Income, or Multi-Item link flows
# https://plaid.com/docs/api/users/#usercreate
post '/api/create_user_token' do
- begin
- client_user_id = 'user_' + SecureRandom.uuid
+ client_user_id = 'user_' + SecureRandom.uuid
+
+ request_data = {
+ # Typically this will be a user ID number from your application.
+ client_user_id: client_user_id
+ }
- request_data = {
- # Typically this will be a user ID number from your application.
- client_user_id: client_user_id
+ if products.any? { |product| product.start_with?("cra_") }
+ # Try with Identity field first (new-style)
+ request_data[:identity] = {
+ name: {
+ given_name: 'Harry',
+ family_name: 'Potter'
+ },
+ date_of_birth: '1980-07-31',
+ phone_numbers: [{
+ data: '+16174567890',
+ primary: true
+ }],
+ emails: [{
+ data: 'harrypotter@example.com',
+ primary: true
+ }],
+ addresses: [{
+ street_1: '4 Privet Drive',
+ city: 'New York',
+ region: 'NY',
+ postal_code: '11111',
+ country: 'US',
+ primary: true
+ }]
}
+ end
- if products.any? { |product| product.start_with?("cra_") }
- # Try with Identity field first (new-style)
- request_data[:identity] = {
- name: {
- given_name: 'Harry',
- family_name: 'Potter'
- },
- date_of_birth: '1980-07-31',
- phone_numbers: [{
- data: '+16174567890',
- primary: true
- }],
- emails: [{
- data: 'harrypotter@example.com',
- primary: true
- }],
- addresses: [{
- street_1: '4 Privet Drive',
- city: 'New York',
- region: 'NY',
- postal_code: '11111',
- country: 'US',
- primary: true
- }]
+ begin
+ user = client.user_create(Plaid::UserCreateRequest.new(request_data))
+ # Store both user_token and user_id
+ user_token = user.user_token if user.user_token
+ user_id = user.user_id if user.user_id
+ content_type :json
+ user.to_hash.to_json
+ rescue Plaid::ApiError => e
+ error_body = JSON.parse(e.response_body) rescue {}
+ if error_body['error_code'] == 'INVALID_FIELD' &&
+ products.any? { |product| product.start_with?("cra_") }
+ retry_request_data = {
+ client_user_id: client_user_id,
+ consumer_report_user_identity: {
+ first_name: 'Harry',
+ last_name: 'Potter',
+ date_of_birth: '1980-07-31',
+ phone_numbers: ['+16174567890'],
+ emails: ['harrypotter@example.com'],
+ primary_address: {
+ city: 'New York',
+ region: 'NY',
+ street: '4 Privet Drive',
+ postal_code: '11111',
+ country: 'US'
+ }
+ }
}
- end
-
- begin
- user = client.user_create(Plaid::UserCreateRequest.new(request_data))
+ user = client.user_create(Plaid::UserCreateRequest.new(retry_request_data))
# Store both user_token and user_id
user_token = user.user_token if user.user_token
user_id = user.user_id if user.user_id
content_type :json
user.to_hash.to_json
- rescue Plaid::ApiError => e
- error_body = JSON.parse(e.response_body) rescue {}
- if error_body['error_code'] == 'INVALID_FIELD' &&
- products.any? { |product| product.start_with?("cra_") }
- retry_request_data = {
- client_user_id: client_user_id,
- consumer_report_user_identity: {
- first_name: 'Harry',
- last_name: 'Potter',
- date_of_birth: '1980-07-31',
- phone_numbers: ['+16174567890'],
- emails: ['harrypotter@example.com'],
- primary_address: {
- city: 'New York',
- region: 'NY',
- street: '4 Privet Drive',
- postal_code: '11111',
- country: 'US'
- }
- }
- }
- user = client.user_create(Plaid::UserCreateRequest.new(retry_request_data))
- # Store both user_token and user_id
- user_token = user.user_token if user.user_token
- user_id = user.user_id if user.user_id
- content_type :json
- user.to_hash.to_json
- else
- raise e
- end
+ else
+ raise e
end
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
end
end
@@ -632,118 +520,103 @@ def nil_if_empty_envvar(field)
# - https://plaid.com/docs/payment-initiation/
# - https://plaid.com/docs/#payment-initiation-create-link-token-request
post '/api/create_link_token_for_payment' do
- begin
- payment_initiation_recipient_create_request = Plaid::PaymentInitiationRecipientCreateRequest.new(
- {
- name: 'Bruce Wayne',
- iban: 'GB33BUKB20201555555555',
- address: {
- street: ['686 Bat Cave Lane'],
- city: 'Gotham',
- postal_code: '99999',
- country: 'GB',
- },
- bacs: {
- account: '26207729',
- sort_code: '560029',
- }
+ payment_initiation_recipient_create_request = Plaid::PaymentInitiationRecipientCreateRequest.new(
+ {
+ name: 'Bruce Wayne',
+ iban: 'GB33BUKB20201555555555',
+ address: {
+ street: ['686 Bat Cave Lane'],
+ city: 'Gotham',
+ postal_code: '99999',
+ country: 'GB',
+ },
+ bacs: {
+ account: '26207729',
+ sort_code: '560029',
}
- )
- create_recipient_response = client.payment_initiation_recipient_create(
- payment_initiation_recipient_create_request
- )
- recipient_id = create_recipient_response.recipient_id
+ }
+ )
+ create_recipient_response = client.payment_initiation_recipient_create(
+ payment_initiation_recipient_create_request
+ )
+ recipient_id = create_recipient_response.recipient_id
- payment_initiation_recipient_get_request = Plaid::PaymentInitiationRecipientGetRequest.new(
- {
- recipient_id: recipient_id
- }
- )
- get_recipient_response = client.payment_initiation_recipient_get(
- payment_initiation_recipient_get_request
- )
+ payment_initiation_recipient_get_request = Plaid::PaymentInitiationRecipientGetRequest.new(
+ {
+ recipient_id: recipient_id
+ }
+ )
+ get_recipient_response = client.payment_initiation_recipient_get(
+ payment_initiation_recipient_get_request
+ )
- payment_initiation_payment_create_request = Plaid::PaymentInitiationPaymentCreateRequest.new(
- {
- recipient_id: recipient_id,
- reference: 'testpayment',
- amount: {
- value: 100.00,
- currency: 'GBP'
- }
+ payment_initiation_payment_create_request = Plaid::PaymentInitiationPaymentCreateRequest.new(
+ {
+ recipient_id: recipient_id,
+ reference: 'testpayment',
+ amount: {
+ value: 100.00,
+ currency: 'GBP'
}
- )
- create_payment_response = client.payment_initiation_payment_create(
- payment_initiation_payment_create_request
- )
- payment_id = create_payment_response.payment_id
+ }
+ )
+ create_payment_response = client.payment_initiation_payment_create(
+ payment_initiation_payment_create_request
+ )
+ payment_id = create_payment_response.payment_id
- link_token_create_request = Plaid::LinkTokenCreateRequest.new(
- {
- client_name: 'Plaid Quickstart',
- user: {
- # This should correspond to a unique id for the current user.
- # Typically, this will be a user ID number from your application.
- # Personally identifiable information, such as an email address or phone number, should not be used here.
- client_user_id: 'user-id'
- },
-
- # Institutions from all listed countries will be shown.
- country_codes: ENV['PLAID_COUNTRY_CODES'].split(','),
- language: 'en',
-
- # The 'payment_initiation' product has to be the only element in the 'products' list.
- products: ['payment_initiation'],
-
- payment_initiation: {
- payment_id: payment_id
- },
- redirect_uri: nil_if_empty_envvar('PLAID_REDIRECT_URI')
- }
- )
- link_response = client.link_token_create(link_token_create_request)
- pretty_print_response(link_response.to_hash)
- content_type :json
- { link_token: link_response.link_token }.to_hash.to_json
-
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ link_token_create_request = Plaid::LinkTokenCreateRequest.new(
+ {
+ client_name: 'Plaid Quickstart',
+ user: {
+ # This should correspond to a unique id for the current user.
+ # Typically, this will be a user ID number from your application.
+ # Personally identifiable information, such as an email address or phone number, should not be used here.
+ client_user_id: 'user-id'
+ },
+
+ # Institutions from all listed countries will be shown.
+ country_codes: ENV['PLAID_COUNTRY_CODES'].split(','),
+ language: 'en',
+
+ # The 'payment_initiation' product has to be the only element in the 'products' list.
+ products: ['payment_initiation'],
+
+ payment_initiation: {
+ payment_id: payment_id
+ },
+ redirect_uri: nil_if_empty_envvar('PLAID_REDIRECT_URI')
+ }
+ )
+ link_response = client.link_token_create(link_token_create_request)
+ pretty_print_response(link_response.to_hash)
+ content_type :json
+ { link_token: link_response.link_token }.to_hash.to_json
end
# Retrieve CRA Base Report and PDF
# Base report: https://plaid.com/docs/check/api/#cracheck_reportbase_reportget
# PDF: https://plaid.com/docs/check/api/#cracheck_reportpdfget
get '/api/cra/get_base_report' do
- begin
- get_response = get_cra_base_report_with_retries(client, user_token, user_id)
- pretty_print_response(get_response.to_hash)
-
- # Use user_token if available, otherwise use user_id
- pdf_params = {}
- if user_token
- pdf_params[:user_token] = user_token
- elsif user_id
- pdf_params[:user_id] = user_id
- end
- pdf_response = client.cra_check_report_pdf_get(
- Plaid::CraCheckReportPDFGetRequest.new(pdf_params)
- )
-
- content_type :json
- {
- report: get_response.report.to_hash,
- pdf: Base64.encode64(File.read(pdf_response))
- }.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
+ get_response = get_cra_base_report_with_retries(client, user_token, user_id)
+ pretty_print_response(get_response.to_hash)
+
+ # Use user_token if available, otherwise use user_id
+ pdf_params = {}
+ if user_token
+ pdf_params[:user_token] = user_token
+ elsif user_id
+ pdf_params[:user_id] = user_id
end
+ pdf_response = client.cra_check_report_pdf_get(
+ Plaid::CraCheckReportPDFGetRequest.new(pdf_params)
+ )
+
+ content_type :json
+ {
+ report: get_response.report.to_hash,
+ pdf: Base64.encode64(File.read(pdf_response))
+ }.to_json
end
def get_cra_base_report_with_retries(plaid_client, user_token, user_id)
@@ -765,32 +638,25 @@ def get_cra_base_report_with_retries(plaid_client, user_token, user_id)
# Income insights: https://plaid.com/docs/check/api/#cracheck_reportincome_insightsget
# PDF w/ income insights: https://plaid.com/docs/check/api/#cracheck_reportpdfget
get '/api/cra/get_income_insights' do
- begin
- get_response = get_income_insights_with_retries(client, user_token, user_id)
- pretty_print_response(get_response.to_hash)
-
- # Use user_token if available, otherwise use user_id
- pdf_params = { add_ons: [Plaid::CraPDFAddOns::INCOME_INSIGHTS] }
- if user_token
- pdf_params[:user_token] = user_token
- elsif user_id
- pdf_params[:user_id] = user_id
- end
- pdf_response = client.cra_check_report_pdf_get(
- Plaid::CraCheckReportPDFGetRequest.new(pdf_params)
- )
-
- content_type :json
- {
- report: get_response.report.to_hash,
- pdf: Base64.encode64(File.read(pdf_response))
- }.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
+ get_response = get_income_insights_with_retries(client, user_token, user_id)
+ pretty_print_response(get_response.to_hash)
+
+ # Use user_token if available, otherwise use user_id
+ pdf_params = { add_ons: [Plaid::CraPDFAddOns::INCOME_INSIGHTS] }
+ if user_token
+ pdf_params[:user_token] = user_token
+ elsif user_id
+ pdf_params[:user_id] = user_id
end
+ pdf_response = client.cra_check_report_pdf_get(
+ Plaid::CraCheckReportPDFGetRequest.new(pdf_params)
+ )
+
+ content_type :json
+ {
+ report: get_response.report.to_hash,
+ pdf: Base64.encode64(File.read(pdf_response))
+ }.to_json
end
def get_income_insights_with_retries(plaid_client, user_token, user_id)
@@ -811,18 +677,11 @@ def get_income_insights_with_retries(plaid_client, user_token, user_id)
# Retrieve CRA Partner Insights
# https://plaid.com/docs/check/api/#cracheck_reportpartner_insightsget
get '/api/cra/get_partner_insights' do
- begin
- response = get_check_partner_insights_with_retries(client, user_token, user_id)
- pretty_print_response(response.to_hash)
+ response = get_check_partner_insights_with_retries(client, user_token, user_id)
+ pretty_print_response(response.to_hash)
- content_type :json
- response.to_hash.to_json
- rescue Plaid::ApiError => e
- error_response = format_error(e)
- pretty_print_response(error_response)
- content_type :json
- error_response.to_json
- end
+ content_type :json
+ response.to_hash.to_json
end
def get_check_partner_insights_with_retries(plaid_client, user_token, user_id)
@@ -849,6 +708,14 @@ def poll_with_retries(ms = 1000, retries_left = 20)
begin
yield
rescue Plaid::ApiError => e
+ error_code = begin
+ JSON.parse(e.response_body)['error_code']
+ rescue JSON::ParserError
+ nil
+ end
+ is_retryable = error_code == 'PRODUCT_NOT_READY' || e.code >= 500
+ raise e unless is_retryable
+
if retries_left > 0
sleep(ms / 1000.0)
poll_with_retries(ms, retries_left - 1) { yield }
@@ -858,16 +725,45 @@ def poll_with_retries(ms = 1000, retries_left = 20)
end
end
-def format_error(err)
- body = JSON.parse(err.response_body)
+post '/api/link_exit_error' do
+ request_body = JSON.parse(request.body.read)
+ puts '[Link Exit Error (frontend)]'
+ pretty_print_response(request_body)
+ content_type :json
+ { status: 'logged' }.to_json
+end
+
+error Plaid::ApiError do |e|
+ begin
+ error_response = format_error(e)
+ pretty_print_response(error_response)
+ status e.code
+ content_type :json
+ error_response.to_json
+ rescue
+ status 500
+ content_type :json
+ { error: { status_code: 500, error_type: 'API_ERROR', error_code: 'INTERNAL_SERVER_ERROR',
+ error_message: 'An unexpected error occurred' } }.to_json
+ end
+end
+
+error do
+ status 500
+ content_type :json
{
error: {
- status_code: err.code,
- error_code: body['error_code'],
- error_message: body['error_message'],
- error_type: body['error_type']
+ status_code: 500,
+ error_type: 'API_ERROR',
+ error_code: 'INTERNAL_SERVER_ERROR',
+ error_message: 'An unexpected error occurred'
}
- }
+ }.to_json
+end
+
+def format_error(err)
+ body = JSON.parse(err.response_body)
+ { error: body.merge('status_code' => err.code) }
end
def pretty_print_response(response)