From df974e1958fef7bcac0b5c75cc5af48d171ab7ac Mon Sep 17 00:00:00 2001 From: Alex Hoffer Date: Thu, 16 Apr 2026 00:27:08 -0700 Subject: [PATCH 1/6] Surface Plaid API errors instead of silently swallowing them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Link errors were invisible to users — the only way to discover errors like INVALID_LINK_CUSTOMIZATION was to open the browser network tab. This was caused by missing error callbacks in the frontend and inconsistent error formatting across backends. Frontend: - Add onExit callback to Link component to surface client-side errors - Display Link exit errors in the UI via a warning callout - Handle both wrapped and flat error formats in generateToken Backends (all languages): - Centralize Plaid API error handling instead of repeating per-endpoint try/catch blocks (Python: @app.errorhandler, Ruby: error block, Java: ExceptionMapper + shared callPlaid() helper) - Python: fix 4 endpoints returning raw error bodies without the { error: ... } wrapper the frontend expects - Java: fix all endpoints NPE-ing on API errors (Retrofit returns errors in the response, not as exceptions) - Node/Ruby: fix pollWithRetries retrying on all errors instead of only PRODUCT_NOT_READY Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.tsx | 15 +- frontend/src/Components/Headers/index.tsx | 18 + frontend/src/Components/Link/index.tsx | 20 + frontend/src/Context/index.tsx | 7 + .../plaid/quickstart/PlaidApiException.java | 16 + .../quickstart/PlaidApiExceptionMapper.java | 14 + .../com/plaid/quickstart/PlaidApiHelper.java | 37 + .../quickstart/QuickstartApplication.java | 1 + .../resources/AccessTokenResource.java | 15 +- .../resources/AccountsResource.java | 8 +- .../quickstart/resources/AssetsResource.java | 15 +- .../quickstart/resources/AuthResource.java | 9 +- .../quickstart/resources/BalanceResource.java | 10 +- .../quickstart/resources/CraResource.java | 42 +- .../resources/HoldingsResource.java | 10 +- .../resources/IdentityResource.java | 10 +- .../InvestmentTransactionsResource.java | 10 +- .../quickstart/resources/ItemResource.java | 21 +- .../resources/LinkTokenResource.java | 11 +- .../LinkTokenWithPaymentResource.java | 28 +- .../resources/PaymentInitiationResource.java | 26 +- .../resources/PublicTokenResource.java | 9 +- .../quickstart/resources/SignalResource.java | 16 +- .../resources/StatementsResource.java | 21 +- .../resources/TransactionsResource.java | 7 +- .../resources/TransferAuthorizeResource.java | 22 +- .../resources/TransferCreateResource.java | 11 +- .../resources/UserTokenResource.java | 105 +- node/index.js | 17 +- python/server.py | 734 ++++++-------- ruby/app.rb | 937 ++++++++---------- 31 files changed, 1017 insertions(+), 1205 deletions(-) create mode 100644 java/src/main/java/com/plaid/quickstart/PlaidApiException.java create mode 100644 java/src/main/java/com/plaid/quickstart/PlaidApiExceptionMapper.java create mode 100644 java/src/main/java/com/plaid/quickstart/PlaidApiHelper.java diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cecca0bb1..f246eea21 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -80,11 +80,22 @@ const App = () => { const response = await fetch(path, { method: "POST", }); + const data = await response.json(); if (!response.ok) { - dispatch({ type: "SET_STATE", state: { linkToken: null } }); + dispatch({ + type: "SET_STATE", + state: { + linkToken: null, + linkTokenError: 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}`, + }, + }, + }); return; } - const data = await response.json(); if (data) { if (data.error != null) { dispatch({ diff --git a/frontend/src/Components/Headers/index.tsx b/frontend/src/Components/Headers/index.tsx index c175ab799..b3d168649 100644 --- a/frontend/src/Components/Headers/index.tsx +++ b/frontend/src/Components/Headers/index.tsx @@ -19,6 +19,7 @@ const Header = () => { isItemAccess, backend, linkTokenError, + linkExitError, isPaymentInitiation, } = useContext(Context); @@ -90,6 +91,23 @@ 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}
+ )} +
+ )} ) : ( <> diff --git a/frontend/src/Components/Link/index.tsx b/frontend/src/Components/Link/index.tsx index 3b94ac7c7..d59a6ef6e 100644 --- a/frontend/src/Components/Link/index.tsx +++ b/frontend/src/Components/Link/index.tsx @@ -8,6 +8,25 @@ const Link = () => { const { linkToken, isPaymentInitiation, isCraProductsExclusively, dispatch } = useContext(Context); + const onExit = React.useCallback( + (error: any) => { + if (error != null) { + dispatch({ + type: "SET_STATE", + state: { + linkExitError: { + error_type: error.error_type || "", + error_code: error.error_code || "", + error_message: error.error_message || "", + display_message: error.display_message || "", + }, + }, + }); + } + }, + [dispatch] + ); + const onSuccess = React.useCallback( (public_token: string) => { // If the access_token is needed, send public_token to server @@ -61,6 +80,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..2a620147a 100644 --- a/frontend/src/Context/index.tsx +++ b/frontend/src/Context/index.tsx @@ -19,6 +19,12 @@ interface QuickstartState { error_code: string; error_type: string; }; + linkExitError: { + error_message: string; + error_code: string; + error_type: string; + display_message: string; + } | null; } const initialState: QuickstartState = { @@ -40,6 +46,7 @@ const initialState: QuickstartState = { error_code: "", error_message: "", }, + linkExitError: null, }; type QuickstartAction = { 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..fa25ff66c --- /dev/null +++ b/java/src/main/java/com/plaid/quickstart/PlaidApiException.java @@ -0,0 +1,16 @@ +package com.plaid.quickstart; + +import java.util.Map; + +public class PlaidApiException extends RuntimeException { + private final Map errorResponse; + + public PlaidApiException(Map errorResponse) { + super(errorResponse.toString()); + this.errorResponse = errorResponse; + } + + public Map getErrorResponse() { + return errorResponse; + } +} 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..462d257d7 --- /dev/null +++ b/java/src/main/java/com/plaid/quickstart/PlaidApiExceptionMapper.java @@ -0,0 +1,14 @@ +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) { + return Response.ok(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..7c623d392 --- /dev/null +++ b/java/src/main/java/com/plaid/quickstart/PlaidApiHelper.java @@ -0,0 +1,37 @@ +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)); + } 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); + } + 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..560d9e7d6 100644 --- a/java/src/main/java/com/plaid/quickstart/QuickstartApplication.java +++ b/java/src/main/java/com/plaid/quickstart/QuickstartApplication.java @@ -107,6 +107,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)); 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..8072d1146 100644 --- a/java/src/main/java/com/plaid/quickstart/resources/AssetsResource.java +++ b/java/src/main/java/com/plaid/quickstart/resources/AssetsResource.java @@ -9,6 +9,7 @@ import com.plaid.client.model.AssetReportGetRequest; import com.plaid.client.model.AssetReportGetResponse; import com.plaid.client.model.AssetReportPDFGetRequest; +import com.plaid.quickstart.PlaidApiHelper; import com.plaid.quickstart.QuickstartApplication; import com.plaid.client.model.PlaidError; import com.plaid.client.model.PlaidErrorType; @@ -46,11 +47,10 @@ 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; @@ -76,11 +76,10 @@ 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("pdf", pdf); 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..dfc2310e2 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; @@ -49,8 +51,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 +60,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 +86,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 +97,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 +120,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 +129,22 @@ 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) { + if (i == 20) { + throw e; + } try { Thread.sleep(5000); - } catch (Exception e) { - throw Throwables.propagate(e); + } 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/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..3f53b4925 100644 --- a/node/index.js +++ b/node/index.js @@ -870,17 +870,22 @@ const pollWithRetries = ( new Promise((resolve, reject) => { requestCallback() .then(resolve) - .catch(() => { + .catch((error) => { + const errorCode = error?.response?.data?.error_code; + if (errorCode !== 'PRODUCT_NOT_READY') { + 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); }); }); diff --git a/python/server.py b/python/server.py index e8a34a4fb..336776f9c 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' - ) + 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 - ) + ) + 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 ) - 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 - ) + ) + 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. @@ -845,5 +759,9 @@ def format_error(e): return {'error': {'status_code': e.status, 'display_message': response['error_message'], 'error_code': response['error_code'], 'error_type': response['error_type']}} +@app.errorhandler(plaid.ApiException) +def handle_plaid_error(e): + return jsonify(format_error(e)) + if __name__ == '__main__': app.run(port=int(os.getenv('PORT', 8000))) diff --git a/ruby/app.rb b/ruby/app.rb index 0fd8b3875..8f04a8d4f 100644 --- a/ruby/app.rb +++ b/ruby/app.rb @@ -76,162 +76,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 +191,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 +231,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 +254,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 +277,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 - - # Generate unique transaction ID using timestamp and random component - client_transaction_id = "txn-#{Time.now.to_i}-#{SecureRandom.hex(4)}" + # 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 - signal_request_params = { - access_token: access_token, - account_id: account_id, - client_transaction_id: client_transaction_id, - amount: 100.00 - } + # Generate unique transaction ID using timestamp and random component + client_transaction_id = "txn-#{Time.now.to_i}-#{SecureRandom.hex(4)}" - 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 +518,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 +636,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 +675,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 +706,9 @@ def poll_with_retries(ms = 1000, retries_left = 20) begin yield rescue Plaid::ApiError => e + json_response = JSON.parse(e.response_body) + raise e unless json_response['error_code'] == 'PRODUCT_NOT_READY' + if retries_left > 0 sleep(ms / 1000.0) poll_with_retries(ms, retries_left - 1) { yield } @@ -858,6 +718,13 @@ def poll_with_retries(ms = 1000, retries_left = 20) end end +error Plaid::ApiError do |e| + error_response = format_error(e) + pretty_print_response(error_response) + content_type :json + error_response.to_json +end + def format_error(err) body = JSON.parse(err.response_body) { From 6ff2e25b68d0ae2d86b327cc8d18bbea772251a3 Mon Sep 17 00:00:00 2001 From: Alex Hoffer Date: Thu, 16 Apr 2026 00:38:07 -0700 Subject: [PATCH 2/6] Fix error response parsing and Java polling retry logic App.tsx: Check response.ok before calling response.json() so non-JSON error responses (e.g. HTML 502) don't throw an unhandled exception. CraResource.java: Only retry on PRODUCT_NOT_READY errors instead of retrying on all PlaidApiExceptions. Also align retry sleep to 1000ms to match other backends. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.tsx | 28 ++++++++++++------- .../quickstart/resources/CraResource.java | 8 +++++- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f246eea21..b26323396 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -80,22 +80,30 @@ const App = () => { const response = await fetch(path, { method: "POST", }); - const data = await response.json(); if (!response.ok) { + 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: 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}`, - }, - }, + state: { linkToken: null, linkTokenError: errorDetail }, }); return; } + const data = await response.json(); if (data) { if (data.error != null) { dispatch({ 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 dfc2310e2..fdff12eff 100644 --- a/java/src/main/java/com/plaid/quickstart/resources/CraResource.java +++ b/java/src/main/java/com/plaid/quickstart/resources/CraResource.java @@ -19,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; @@ -136,11 +138,15 @@ private T pollWithRetries(Call requestCallback) throws IOException { try { return PlaidApiHelper.callPlaid(call); } catch (PlaidApiException e) { + Map error = (Map) e.getErrorResponse().get("error"); + if (error == null || !"PRODUCT_NOT_READY".equals(error.get("error_code"))) { + throw e; + } if (i == 20) { throw e; } try { - Thread.sleep(5000); + Thread.sleep(1000); } catch (InterruptedException ie) { throw Throwables.propagate(ie); } From ea8f3b1cd6befc0373c4e703d970d4f530dda30d Mon Sep 17 00:00:00 2001 From: Alex Hoffer Date: Thu, 16 Apr 2026 00:45:51 -0700 Subject: [PATCH 3/6] Also retry polling on transient 5xx errors PRODUCT_NOT_READY is the expected retryable error, but transient 500s from the API should also be retried rather than immediately failing. This aligns all five backends on the same retry logic: retry on PRODUCT_NOT_READY or 5xx, fail fast on everything else. Co-Authored-By: Claude Opus 4.6 (1M context) --- go/server.go | 6 ++++-- .../java/com/plaid/quickstart/resources/CraResource.java | 4 +++- node/index.js | 4 +++- python/server.py | 3 ++- ruby/app.rb | 3 ++- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/go/server.go b/go/server.go index 999a6984f..f6d5e961a 100644 --- a/go/server.go +++ b/go/server.go @@ -1057,8 +1057,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/resources/CraResource.java b/java/src/main/java/com/plaid/quickstart/resources/CraResource.java index fdff12eff..3f2596bbe 100644 --- a/java/src/main/java/com/plaid/quickstart/resources/CraResource.java +++ b/java/src/main/java/com/plaid/quickstart/resources/CraResource.java @@ -139,7 +139,9 @@ private T pollWithRetries(Call requestCallback) throws IOException { return PlaidApiHelper.callPlaid(call); } catch (PlaidApiException e) { Map error = (Map) e.getErrorResponse().get("error"); - if (error == null || !"PRODUCT_NOT_READY".equals(error.get("error_code"))) { + 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) { diff --git a/node/index.js b/node/index.js index 3f53b4925..b597a6d93 100644 --- a/node/index.js +++ b/node/index.js @@ -872,7 +872,9 @@ const pollWithRetries = ( .then(resolve) .catch((error) => { const errorCode = error?.response?.data?.error_code; - if (errorCode !== 'PRODUCT_NOT_READY') { + const statusCode = error?.response?.status; + const isRetryable = errorCode === 'PRODUCT_NOT_READY' || (statusCode >= 500 && statusCode < 600); + if (!isRetryable) { reject(error); return; } diff --git a/python/server.py b/python/server.py index 336776f9c..cfad1609f 100644 --- a/python/server.py +++ b/python/server.py @@ -743,7 +743,8 @@ def poll_with_retries(request_callback, ms=1000, retries_left=20): return request_callback() except plaid.ApiException as e: response = json.loads(e.body) - if response['error_code'] != 'PRODUCT_NOT_READY': + is_retryable = response['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 diff --git a/ruby/app.rb b/ruby/app.rb index 8f04a8d4f..db210ec95 100644 --- a/ruby/app.rb +++ b/ruby/app.rb @@ -707,7 +707,8 @@ def poll_with_retries(ms = 1000, retries_left = 20) yield rescue Plaid::ApiError => e json_response = JSON.parse(e.response_body) - raise e unless json_response['error_code'] == 'PRODUCT_NOT_READY' + is_retryable = json_response['error_code'] == 'PRODUCT_NOT_READY' || e.code >= 500 + raise e unless is_retryable if retries_left > 0 sleep(ms / 1000.0) From 785a097bec05a7c02c6b473070d9dbce525856b5 Mon Sep 17 00:00:00 2001 From: Alex Hoffer Date: Thu, 16 Apr 2026 00:49:33 -0700 Subject: [PATCH 4/6] Fix assets polling loop and harden JSON parsing in poll helpers AssetsResource.java: Replace broken polling loop (had no-op .equals() checks that never gated retries, and would NPE on error responses) with PlaidApiHelper.callPlaid() and the same PRODUCT_NOT_READY/5xx retry logic used everywhere else. Python/Ruby poll_with_retries: Wrap JSON parsing of error body in try/catch so a non-JSON 5xx from a proxy doesn't crash the request instead of retrying. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../quickstart/resources/AssetsResource.java | 46 ++++++++++--------- python/server.py | 8 +++- ruby/app.rb | 8 +++- 3 files changed, 36 insertions(+), 26 deletions(-) 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 8072d1146..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,13 +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; @@ -25,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 { @@ -53,22 +47,30 @@ public Map getAssetReport() throws IOException { 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); } } } @@ -81,7 +83,7 @@ public Map getAssetReport() throws IOException { 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/python/server.py b/python/server.py index cfad1609f..8259c363b 100644 --- a/python/server.py +++ b/python/server.py @@ -742,8 +742,12 @@ 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) - is_retryable = response['error_code'] == 'PRODUCT_NOT_READY' or e.status >= 500 + 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: diff --git a/ruby/app.rb b/ruby/app.rb index db210ec95..01d99546b 100644 --- a/ruby/app.rb +++ b/ruby/app.rb @@ -706,8 +706,12 @@ def poll_with_retries(ms = 1000, retries_left = 20) begin yield rescue Plaid::ApiError => e - json_response = JSON.parse(e.response_body) - is_retryable = json_response['error_code'] == 'PRODUCT_NOT_READY' || e.code >= 500 + 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 From 9ebad66d7d47dc261fa32b222ad1e6eced60e6fb Mon Sep 17 00:00:00 2001 From: Alex Hoffer Date: Thu, 16 Apr 2026 17:54:22 -0700 Subject: [PATCH 5/6] Fix error reporting across Python, Ruby, and Java backends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python was missing the error_message field entirely — it mapped error_message into display_message and dropped the original, so the frontend had nothing to show. Now spreads the full Plaid error body, matching Node's approach. Ruby errors were invisible because Sinatra's show_exceptions middleware replaced our JSON error response with an HTML stack trace page. The frontend couldn't parse it and fell back to hardcoded "UNKNOWN" / "API_ERROR" defaults. Disabled show_exceptions and dump_errors, added a defensive rescue inside the Plaid error handler, and added a generic catch-all error handler that returns clean JSON. Java was missing display_message in its error construction, so the frontend could never show user-friendly error messages. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/plaid/quickstart/PlaidApiHelper.java | 3 +- python/server.py | 15 ++++-- ruby/app.rb | 46 ++++++++++++++----- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/java/src/main/java/com/plaid/quickstart/PlaidApiHelper.java b/java/src/main/java/com/plaid/quickstart/PlaidApiHelper.java index 7c623d392..143bf9c30 100644 --- a/java/src/main/java/com/plaid/quickstart/PlaidApiHelper.java +++ b/java/src/main/java/com/plaid/quickstart/PlaidApiHelper.java @@ -23,6 +23,7 @@ public static T callPlaid(Call call) throws IOException { 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"); @@ -30,7 +31,7 @@ public static T callPlaid(Call call) throws IOException { } Map result = new HashMap<>(); result.put("error", error); - throw new PlaidApiException(result); + throw new PlaidApiException(result, response.code()); } return response.body(); } diff --git a/python/server.py b/python/server.py index 8259c363b..4284ad6be 100644 --- a/python/server.py +++ b/python/server.py @@ -761,12 +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): - return jsonify(format_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 01d99546b..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. @@ -723,23 +725,45 @@ def poll_with_retries(ms = 1000, retries_left = 20) end end -error Plaid::ApiError do |e| - error_response = format_error(e) - pretty_print_response(error_response) +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 - error_response.to_json + { status: 'logged' }.to_json end -def format_error(err) - body = JSON.parse(err.response_body) +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) From d32f0fa8478f0c50417c62e4ae2c3f972d77eb90 Mon Sep 17 00:00:00 2001 From: Alex Hoffer Date: Fri, 17 Apr 2026 13:07:38 -0700 Subject: [PATCH 6/6] Add link exit error endpoints, helpful error tips, and fix status codes Frontend: send Link exit errors to backend /api/link_exit_error endpoint for server-side logging, include institution_name from Link metadata, and add contextual tips for INVALID_LINK_CUSTOMIZATION and INSTITUTION_REGISTRATION_REQUIRED errors. All backends: add /api/link_exit_error endpoint (Go, Node, Java). Go: fix renderError to log errors and return proper HTTP status codes instead of always returning 200. Node: fix error middleware to return proper HTTP status codes and handle cases where error.response is undefined. Java: add statusCode to PlaidApiException so the ExceptionMapper can return the actual HTTP status instead of 200. Register LinkExitErrorResource. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/Components/Error/index.tsx | 35 ++++++++++++ frontend/src/Components/Headers/index.tsx | 53 +++++++++++++++++++ frontend/src/Components/Link/index.tsx | 23 ++++---- frontend/src/Context/index.tsx | 1 + go/server.go | 24 ++++++++- .../plaid/quickstart/PlaidApiException.java | 8 ++- .../quickstart/PlaidApiExceptionMapper.java | 5 +- .../quickstart/QuickstartApplication.java | 2 + .../resources/LinkExitErrorResource.java | 28 ++++++++++ node/index.js | 16 ++++-- 10 files changed, 179 insertions(+), 16 deletions(-) create mode 100644 java/src/main/java/com/plaid/quickstart/resources/LinkExitErrorResource.java 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. + +
+ )}