Linked Institutions

Linked Institution Integration Guide

Linking a user's external bank account allows them to connect their existing financial institution accounts to your platform. This enables use cases like ACH transfers, account verification, and transaction aggregation.

The Spidr ZTM API supports two integration methods:

  • SDK Experience - Embed Plaid Link in your frontend (recommended for web/mobile apps)
  • Hosted URL Experience - Redirect users to Plaid's hosted page (simpler, no frontend work)

TL;DR

SDK Experience (Plaid Link UI)

  1. Call POST /ztm/v1/linked-institution/session/create → Get linkToken + linkedInstitutionSessionId
  2. Open Plaid Link with linkToken → User authenticates → Get publicToken in onSuccess callback
  3. Call POST /ztm/v1/linked-institution/session/complete with publicToken

Hosted URL Experience

  1. Call POST /ztm/v1/linked-institution/session/create with completionRedirectUri → Get hostedLinkUrl + linkedInstitutionSessionId
  2. Redirect user to hostedLinkUrl → User completes flow → User is redirected back to your completionRedirectUri
  3. Call POST /ztm/v1/linked-institution/session/complete with linkedInstitutionSessionId

No webhooks required - the redirect URI brings the user back to your app after completion.

Sandbox testing: Use user_good / pass_good as credentials in Plaid Link.


Choosing an Integration Method

AspectSDK ExperienceHosted URL Experience
Setup complexityRequires frontend codeNo frontend work
User experienceSeamless, in-appRedirects to Plaid
OAuth handlingYou handle redirectsPlaid handles everything
Best forWeb apps, native mobileWebviews, email/SMS flows, embedded clients
Completion detectiononSuccess callbackcompletionRedirectUri (no webhooks needed)
publicToken needed?Yes, in Complete SessionNo - just session ID

Integration Flow Diagrams

SDK Experience Flow

Use this when you embed Plaid Link directly in your frontend application.

Hosted URL Experience Flow

Use this when you want Plaid to host the entire Link experience. No frontend integration or webhooks required.


Step 1: Create a Linked Institution Session

Your backend calls the Spidr ZTM API to create a session. The response includes both a linkToken (for SDK) and hostedLinkUrl (for hosted experience).

API Reference: POST /ztm/v1/linked-institution/session/create

Request

POST /ztm/v1/linked-institution/session/create
Content-Type: application/json
x-client-request-id: <unique-request-id>
apikey: <your-api-key>

{
  "userId": "669dd8fa866f2d225027b3ee",
  "productId": "67ca1fa8599e6b6e661cd9b4",
  "services": ["account_linking", "account_verification"],
  "provider": "plaid",
  "providerOptions": {
    "displayName": "My App"
  }
}

Request Fields

FieldTypeRequiredDescription
userIdstringYesThe Spidr user ID
productIdstringYesYour product ID (contains Plaid configuration)
servicesstring[]YesServices to enable (see below)
providerstringYesProvider to use: "plaid"
providerOptions.displayNamestringYes (Plaid)Name shown in Plaid Link UI
providerOptions.languagestringNoLanguage for Plaid Link UI (e.g. "en", "es")
providerOptions.accountSelectionbooleanNoAllow user to select specific accounts
providerOptions.redirectUristringNoOAuth redirect URI (SDK experience with OAuth banks)
providerOptions.androidPackageNamestringNoAndroid package name for deep linking
providerOptions.hostedLinkUrl.completionRedirectUristringNoURL to redirect user after completing the hosted flow
providerOptions.hostedLinkUrl.deliveryMethodstringNoHow to deliver the hosted link: "email" or "sms"
providerOptions.hostedLinkUrl.isMobileAppbooleanNoWhether the flow is for a mobile app
providerOptions.hostedLinkUrl.urlLifetimeSecondsnumberNoCustom lifetime for the hosted link URL (60–86400)
linkedInstitutionIdstringNoFor re-authentication of existing institutions

Available Services

ServiceDescription
account_linkingBasic account linking for ACH transfers
account_verificationVerify account ownership
transaction_aggregationAccess transaction history (requires account_linking)

Response

{
  "status": "success",
  "data": {
    "linkedInstitutionSessionId": "678abc123def456789012345",
    "spidrActionId": "678abc123def456789012346",
    "provider": "plaid",
    "providerData": {
      "linkToken": "link-sandbox-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "hostedLinkUrl": "https://secure.plaid.com/link/xxxxxxxxxx",
      "expiration": "2027-01-15T12:00:00Z"
    }
  }
}

Key Response Fields:

FieldUse With
linkedInstitutionSessionIdBoth methods - needed for Complete Session
providerData.linkTokenSDK Experience - Pass to Plaid Link SDK
providerData.hostedLinkUrlHosted Experience - Redirect user here

Step 2A: SDK Experience (Plaid Link UI)

Use this method when you want to embed Plaid Link directly in your web or mobile application.

Prerequisites

npm install react-plaid-link

React Implementation

import { useState, useCallback, useEffect } from 'react';
import { usePlaidLink, PlaidLinkOnSuccess, PlaidLinkOnExit } from 'react-plaid-link';

function LinkAccountFlow({ userId, productId }: { userId: string; productId: string }) {
  const [linkToken, setLinkToken] = useState<string | null>(null);
  const [sessionId, setSessionId] = useState<string | null>(null);

  // Step 1: Get link token from your backend
  const initiateLinking = async () => {
    const response = await fetch('/api/linked-institution/create-session', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        userId,
        productId,
        provider: 'plaid',
        services: ['account_linking'],
        providerOptions: { displayName: 'My App' }
      }),
    });
    
    const data = await response.json();
    
    // Use linkToken for SDK experience
    setLinkToken(data.data.providerData.linkToken);
    setSessionId(data.data.linkedInstitutionSessionId);
  };

  // Step 3: Complete session after Plaid success
  const handlePlaidSuccess: PlaidLinkOnSuccess = useCallback(async (publicToken, metadata) => {
    console.log('Plaid Link success:', { publicToken, metadata });
    
    // Send publicToken to complete the session
    await fetch('/api/linked-institution/complete-session', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        linkedInstitutionSessionId: sessionId,
        providerOptions: { 
          publicToken: publicToken  // Required for SDK experience
        },
      }),
    });
  }, [sessionId]);

  const handlePlaidExit: PlaidLinkOnExit = useCallback((err, metadata) => {
    if (err) {
      console.error('Plaid Link error:', err);
      if (err.error_code === 'INVALID_LINK_TOKEN') {
        // Token expired - reinitiate the flow
        setLinkToken(null);
      }
    }
  }, []);

  // Initialize Plaid Link with the linkToken
  const { open, ready } = usePlaidLink({
    token: linkToken,
    onSuccess: handlePlaidSuccess,
    onExit: handlePlaidExit,
  });

  // Auto-open when ready
  useEffect(() => {
    if (linkToken && ready) {
      open();
    }
  }, [linkToken, ready, open]);

  return (
    <button onClick={initiateLinking} disabled={!!linkToken}>
      Link Bank Account
    </button>
  );
}

Vanilla JavaScript Implementation

<script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>

<script>
async function linkBankAccount() {
  // Step 1: Create session
  const response = await fetch('/api/linked-institution/create-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      userId: 'user-123',
      productId: 'product-456',
      provider: 'plaid',
      services: ['account_linking'],
      providerOptions: { displayName: 'My App' }
    }),
  });
  
  const data = await response.json();
  const sessionId = data.data.linkedInstitutionSessionId;
  const linkToken = data.data.providerData.linkToken;
  
  // Step 2: Open Plaid Link with linkToken
  const handler = Plaid.create({
    token: linkToken,
    onSuccess: async (publicToken, metadata) => {
      // Step 3: Complete session with publicToken
      await fetch('/api/linked-institution/complete-session', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          linkedInstitutionSessionId: sessionId,
          providerOptions: { publicToken },
        }),
      });
      alert('Bank account linked!');
    },
    onExit: (err, metadata) => {
      if (err) console.error('Error:', err);
    },
  });
  
  handler.open();
}
</script>

<button onclick="linkBankAccount()">Connect Bank Account</button>

Key Points - SDK Experience

  • The publicToken is returned in the onSuccess callback - you must pass it to Complete Session
  • The publicToken is short-lived (~30 minutes) - complete the session promptly
  • Handle INVALID_LINK_TOKEN errors by re-creating the session

Step 2B: Hosted URL Experience

Use this method when you don't want to build frontend integration. Plaid hosts the entire Link experience.

When to Use Hosted URL

  • No frontend work required - Just redirect users
  • No webhooks needed - Use completionRedirectUri to bring users back to your app
  • Webview-based apps - Avoids SDK compatibility issues
  • Email/SMS flows - Send the link directly to users
  • Embedded clients - PSPs and other integrations
  • OAuth handling - Plaid manages all redirects

Implementation

// Backend: Create session with redirect URI
async function createHostedLinkSession(userId, productId) {
  // First, create a tracking ID or use your own session reference
  const internalRef = generateUniqueId();
  
  const response = await fetch('https://api.gospidr.com/ztm/v1/linked-institution/session/create', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'apikey': process.env.SPIDR_API_KEY,
    },
    body: JSON.stringify({
      userId,
      productId,
      provider: 'plaid',
      services: ['account_linking'],
      providerOptions: { 
        displayName: 'My App',
        hostedLinkUrl: {
          // User will be redirected here after completing the flow
          completionRedirectUri: `https://yourapp.com/link-complete?ref=${internalRef}`
        }
      }
    }),
  });
  
  const data = await response.json();
  const sessionId = data.data.linkedInstitutionSessionId;
  
  // Store the mapping so you can look up the Spidr session ID later
  await storeSessionMapping(internalRef, sessionId);
  
  return {
    sessionId,
    hostedLinkUrl: data.data.providerData.hostedLinkUrl,
  };
}

// Redirect user to Plaid's hosted page
function redirectToHostedLink(hostedLinkUrl) {
  window.location.href = hostedLinkUrl;
}

Handling Completion (Hosted Experience)

When you include completionRedirectUri under providerOptions.hostedLinkUrl in your Create Session request, Plaid automatically redirects the user back to your app after they complete (or exit) the flow. No webhooks required.

Handle the redirect:

// Backend route: /link-complete
app.get('/link-complete', async (req, res) => {
  const { ref } = req.query;
  
  // Look up the Spidr session ID from your stored mapping
  const sessionId = await getSessionMapping(ref);
  
  // Complete the session with Spidr
  const result = await fetch('https://api.gospidr.com/ztm/v1/linked-institution/session/complete', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'apikey': process.env.SPIDR_API_KEY,
    },
    body: JSON.stringify({
      linkedInstitutionSessionId: sessionId,
    }),
  });
  
  const data = await result.json();
  
  if (data.status === 'success' && data.data.linkedInstitutions?.length > 0) {
    // Success! Show confirmation to user
    res.redirect('/dashboard?linked=success');
  } else {
    // User exited without completing, or error occurred
    res.redirect('/dashboard?linked=cancelled');
  }
});

Note: The redirect happens whether the user successfully linked OR exited early. Always call Complete Session to check the actual result.

Key Points - Hosted Experience

  • Include completionRedirectUri under providerOptions.hostedLinkUrl in your Create Session request
  • Use hostedLinkUrl from the response to redirect the user
  • No publicToken needed when calling Complete Session - Spidr handles token exchange
  • No webhooks needed - user is redirected back to your app after completion
  • The redirect happens for both success AND exit - always call Complete Session to verify the result
  • The hosted link URL has a default lifetime of ~30 minutes (configurable)

Step 3: Complete the Session

API Reference: POST /ztm/v1/linked-institution/session/complete

Request - SDK Experience

When using the Plaid Link SDK, you must include the publicToken:

POST /ztm/v1/linked-institution/session/complete
Content-Type: application/json
apikey: <your-api-key>

{
  "linkedInstitutionSessionId": "678abc123def456789012345",
  "providerOptions": {
    "publicToken": "public-sandbox-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  }
}

Request - Hosted Experience

When using the hosted URL experience, only pass the session ID:

POST /ztm/v1/linked-institution/session/complete
Content-Type: application/json
apikey: <your-api-key>

{
  "linkedInstitutionSessionId": "678abc123def456789012345"
}

Response

Both methods return the same response structure:

{
  "status": "success",
  "data": {
    "linkedInstitutionSessionId": "698505f8649259b23141a5f1",
    "spidrActionId": "6985060d649259b23141a5fe",
    "linkedInstitutions": [
      {
        "id": "6985060d649259b23141a601",
        "userId": "6984f595c07bda200b0e92d6",
        "productId": "6823a83e59a0f48f78787c29",
        "provider": "plaid",
        "name": "Huntington Bank - Personal & Business",
        "status": "success",
        "vendorServices": ["account_linking", "transaction_aggregation"],
        "accounts": [
          {
            "linkedInstitutionAccountId": "6985060d649259b23141a602",
            "name": "Plaid Checking",
            "type": "depository.checking",
            "accountNumber": "************0000",
            "routingNumber": "011401533",
            "wireRoutingNumber": "021000021",
            "balance": 110,
            "availableBalance": 100,
            "currencyCode": "USD",
            "provider": "plaid",
            "providerAccountId": "rPQAVQboZpTayn1Er7QXhgQl4pxK1qTrKMQ46",
            "providerDetails": {
              "account_id": "rPQAVQboZpTayn1Er7QXhgQl4pxK1qTrKMQ46",
              "mask": "0000",
              "type": "depository",
              "subtype": "checking"
            }
          },
          {
            "linkedInstitutionAccountId": "6985060d649259b23141a603",
            "name": "Plaid Saving",
            "type": "depository.savings",
            "accountNumber": "************1111",
            "routingNumber": "011401533",
            "balance": 210,
            "availableBalance": 200,
            "currencyCode": "USD",
            "provider": "plaid",
            "providerAccountId": "zL8Rq831bkTZzwnbNX8gcn98vLq4roFAPmVrm"
          }
        ],
        "updatedAt": "2026-02-05T21:05:17.817Z"
      }
    ],
    "linkedInstitutionsErrors": []
  }
}

Key fields to save:

  • linkedInstitutions[].id - The linked institution ID (for refresh, remove operations)
  • linkedInstitutions[].accounts[].linkedInstitutionAccountId - The account ID (for ACH transfers, balance checks)

Post-Linking Operations

After successfully linking an institution, you can perform these operations:

EndpointMethodDescription
/ztm/v1/linked-institution/listGETList all linked institutions for a user
/ztm/v1/linked-institution/{id}DELETERemove a linked institution
/ztm/v1/linked-institution/{id}/refreshPOSTRefresh institution data
/ztm/v1/linked-institution/{id}/transactionsGETFetch transactions from linked institution
/ztm/v1/linked-institution/account-balance-checkPOSTCheck account balance

Example: Check Account Balance

POST /ztm/v1/linked-institution/account-balance-check
Content-Type: application/json
apikey: <your-api-key>

{
  "linkedInstitutionAccountId": "6985060d649259b23141a602",
  "userId": "6984f595c07bda200b0e92d6",
  "requestedAmount": 105.55,
  "accountMinimumBalance": 200
}
FieldTypeRequiredDescription
linkedInstitutionAccountIdstringYesThe linked institution account to check
userIdstringYesThe Spidr user ID
requestedAmountnumberYesThe amount to verify the account can cover
accountMinimumBalancenumberNoMinimum balance threshold (defaults to 0)

Error Handling

Common Errors

ErrorCauseSolution
INVALID_LINK_TOKENLink token expired (~30 min)Create a new session
PLAID_LINK_TOKEN_GET_NO_RESULTS_FOUNDProduct not configured for PlaidVerify product has Plaid credentials
INVALID_CREDENTIALSUser entered wrong bank credentialsUser can retry in Plaid Link

Plaid Link Exit Statuses

When a user exits without completing, check metadata.status:

StatusDescription
requires_credentialsUser exited before entering credentials
requires_codeUser exited on MFA screen
institution_not_foundUser couldn't find their bank
institution_not_supportedSelected bank not supported

Sandbox Testing

Test Credentials

UsernamePasswordBehavior
user_goodpass_goodSuccess
user_badpass_badInvalid credentials
user_goodmfa_deviceMFA device selection
user_goodmfa_questionsSecurity questions

Sandbox Test Institution

The default sandbox test institution is First Platypus Bank (ins_109508). This is Plaid's standard sandbox bank and is used by default when calling the simulate-link endpoint.

Simulate Link (API-Only Testing)

For automated testing without the Plaid UI:

POST /ztm/v1/linked-institution/session/simulate-link
Content-Type: application/json
apikey: <your-api-key>

{
  "linkedInstitutionSessionId": "678abc123def456789012345",
  "institutionId": "ins_109508"
}

Returns a publicToken you can use with Complete Session.


Quick Reference

Integration Method Comparison

StepSDK ExperienceHosted Experience
Create SessionSame endpointInclude providerOptions.hostedLinkUrl.completionRedirectUri
What to useproviderData.linkTokenproviderData.hostedLinkUrl
User flowIn-app Plaid Link UIRedirect to Plaid hosted page
Completion detectiononSuccess callbackhostedLinkUrl.completionRedirectUri (no webhooks)
Complete SessionInclude publicTokenSession ID only

API Endpoints

EndpointMethodDescription
/ztm/v1/linked-institution/session/createPOSTCreate linking session
/ztm/v1/linked-institution/session/completePOSTComplete linking session
/ztm/v1/linked-institution/session/simulate-linkPOSTSandbox: simulate Plaid Link
/ztm/v1/linked-institution/listGETList linked institutions
/ztm/v1/linked-institution/{id}DELETERemove linked institution
/ztm/v1/linked-institution/{id}/refreshPOSTRefresh institution data
/ztm/v1/linked-institution/{id}/transactionsGETFetch linked institution transactions
/ztm/v1/linked-institution/account-balance-checkPOSTCheck account balance

Additional Resources

Spidr Documentation:

Plaid Documentation: