Linking a Plaid Bank Account

Use these operations to let a user connect a bank account to Fluz through Plaid Link. Linking is the entry point for all bank-account funding: once an account is connected, you can read its balance and spend power, pull its transaction history, and repair the connection if it later drops — each covered on its own page.

Transactional Graph Service (TGS) exposes a public GraphQL wrapper over the Fluz Plaid integration. Identity-service remains the system of record for Plaid tokens, bank-account ingestion, balances, historical transactions, and Plaid webhooks. TGS never returns Plaid access tokens or public tokens to your app.

Auth

Call /api/v1/graphql with a Fluz user Bearer access token that includes the MANAGE_PAYMENT scope.

The Plaid fields do not allow Basic auth. Obtain a user access token through the standard TGS auth flow first, then call these fields with:

Authorization: Bearer <fluz-user-access-token>

The link flow at a glance

  1. Create a Link token (createPlaidLinkToken).
  2. Open Plaid Link with that token.
  3. On Plaid's onSuccess, send the returned public_token back to TGS (completePlaidLink).
  4. Store the platformItemId from the completion response — it's the safe public identifier you'll use later to relink.

1. Create a Link token

mutation CreatePlaidLinkToken($input: CreatePlaidLinkTokenInput!) {
  createPlaidLinkToken(input: $input) {
    linkToken
    expiration
    requestId
    mode
  }
}
{
  "input": {}
}

For native Link, pass deviceOs as IOS or ANDROID so identity-service includes the correct Plaid OAuth redirect option.

{
  "input": {
    "deviceOs": "IOS"
  }
}

2. Open Plaid Link

Initialize Plaid Link with the returned linkToken. (See the Web SDK example below.)

3. Complete the link

In Plaid Link's onSuccess, send the returned public_token to TGS.

mutation CompletePlaidLink($input: CompletePlaidLinkInput!) {
  completePlaidLink(input: $input) {
    requiresAddress
    bankAccountId
    bankInstitutionAuthId
    newlyLinkedBankInstitutionAuthId
    bankInstitutionName
    platformItemId
    bankAccounts {
      bankInstitutionAuthId
      bankAccountId
      bankName
      lastFour
      type
      subtype
    }
  }
}
{
  "input": {
    "publicToken": "public-sandbox-..."
  }
}

4. Store the platformItemId

Save platformItemId from the completion response. This is the safe public identifier used later to relink a disconnected connection. If it isn't returned, call getPlaidBankAccounts for the user and store the persisted platformItemId from that response.

If completePlaidLink returns requiresAddress: true, attach an address before the account is usable — see Attach an address below.

Plaid Web SDK example

Plaid's Web SDK script must be loaded directly from Plaid's CDN.

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

Create a Link token through TGS, initialize Plaid Link with that token, and pass Plaid's public_token back to TGS in onSuccess. Calling startPlaidLink() with no argument begins a new link; calling startPlaidLink(existingPlatformItemId) begins a relink (see Relinking Disconnected Bank Accounts).

const FLUZ_GRAPHQL_URL = '<fluz-api-base-url>/api/v1/graphql';
const fluzUserAccessToken = '<fluz-user-access-token>';

const fluzGraphql = async (query, variables) => {
  const response = await fetch(FLUZ_GRAPHQL_URL, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${fluzUserAccessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query, variables }),
  });

  const body = await response.json();
  if (!response.ok || body.errors?.length) {
    throw new Error(body.errors?.[0]?.message ?? 'Fluz GraphQL request failed');
  }

  return body.data;
};

const createPlaidLinkToken = async (platformItemId) => {
  const data = await fluzGraphql(
    `
      mutation CreatePlaidLinkToken($input: CreatePlaidLinkTokenInput!) {
        createPlaidLinkToken(input: $input) {
          linkToken
          mode
        }
      }
    `,
    { input: { platformItemId } },
  );

  return data.createPlaidLinkToken;
};

const completePlaidLink = async (publicToken, platformItemId) => {
  const data = await fluzGraphql(
    `
      mutation CompletePlaidLink($input: CompletePlaidLinkInput!) {
        completePlaidLink(input: $input) {
          requiresAddress
          bankInstitutionAuthId
          newlyLinkedBankInstitutionAuthId
          bankAccountId
          platformItemId
          bankAccounts {
            bankInstitutionAuthId
            bankAccountId
            bankName
            lastFour
            type
            subtype
          }
        }
      }
    `,
    { input: { publicToken, platformItemId } },
  );

  return data.completePlaidLink;
};

const getPlaidBankAccounts = async (input) => {
  const data = await fluzGraphql(
    `
      query GetPlaidBankAccounts($input: PlaidBankAccountFilterInput) {
        getPlaidBankAccounts(input: $input) {
          platformItemId
          bankInstitutionAuthId
          bankAccountId
          bankInstitutionName
          lastFour
          type
          subtype
        }
      }
    `,
    { input },
  );

  return data.getPlaidBankAccounts;
};

const resolvePlatformItemId = async (completion) => {
  if (completion.platformItemId) return completion.platformItemId;

  const bankInstitutionAuthId =
    completion.bankInstitutionAuthId ?? completion.newlyLinkedBankInstitutionAuthId;
  const accounts = await getPlaidBankAccounts({ bankInstitutionAuthId });
  return accounts[0]?.platformItemId;
};

const startPlaidLink = async (platformItemId) => {
  const { linkToken } = await createPlaidLinkToken(platformItemId);

  const handler = window.Plaid.create({
    token: linkToken,
    onSuccess: async (publicToken, metadata) => {
      const result = await completePlaidLink(publicToken, platformItemId);
      const persistedPlatformItemId = await resolvePlatformItemId(result);

      await saveConnectionForRelinkLater({
        platformItemId: persistedPlatformItemId,
        bankInstitutionAuthId:
          result.bankInstitutionAuthId ?? result.newlyLinkedBankInstitutionAuthId,
        bankAccounts: result.bankAccounts,
        plaidLinkSessionId: metadata.link_session_id,
      });
    },
    onExit: (error, metadata) => {
      console.log('Plaid Link exited', { error, metadata });
    },
  });

  handler.open();
};

List linked accounts

Return the safe Plaid bank accounts for the authenticated user.

query GetPlaidBankAccounts($input: PlaidBankAccountFilterInput) {
  getPlaidBankAccounts(input: $input) {
    platformItemId
    bankInstitutionAuthId
    bankInstitutionName
    bankAccountId
    accountName
    lastFour
    type
    subtype
    status
  }
}
{
  "input": {
    "platformItemId": "plaid-item-id"
  }
}

Attach an address

If completePlaidLink returned requiresAddress: true, attach an address to the linked bank institution.

mutation CreatePlaidLinkAddress($input: CreatePlaidLinkAddressInput!) {
  createPlaidLinkAddress(input: $input) {
    addressId
  }
}
{
  "input": {
    "bankInstitutionAuthId": "bank-institution-auth-id",
    "address": {
      "streetAddressLine1": "123 Main St",
      "streetAddressLine2": "Apt 4",
      "city": "New York",
      "state": "NY",
      "postalCode": "10001",
      "country": "US"
    }
  }
}

Remove a bank connection

Remove a Plaid bank institution from the user's account.

mutation RemovePlaidBankInstitution($input: RemovePlaidBankInstitutionInput!) {
  removePlaidBankInstitution(input: $input) {
    removed
    bankInstitutionAuthId
  }
}
{
  "input": {
    "bankInstitutionAuthId": "bank-institution-auth-id",
    "reason": "USER_REQUESTED"
  }
}

Security boundaries

TGS never returns Plaid access tokens, Plaid public tokens, identity-match details, bank account numbers, or routing numbers. Bank account names and nicknames may be user-entered banking labels and are returned only as account metadata.

Identity-service remains responsible for:

  • Plaid token exchange
  • Plaid access token storage
  • Bank institution and bank account creation or repair
  • Balance fetching
  • Historical transaction ingestion
  • Bank institution removal
  • Plaid webhooks

TGS does not expose identity-service directly and does not add a public Plaid webhook route.

Unsupported flows

Manual Plaid micro-deposit linking is intentionally unsupported by this public wrapper. New links and relinks must use the normal Plaid Link flow, which supports balances and historical transactions.

If a user starts a new link for the same institution instead of choosing relink, complete it as a normal new link. Identity-service owns bank-account dedupe and repair; store the returned platformItemId after completion.

Error handling

If completePlaidLink times out or fails after Plaid returned a public_token, first call getPlaidBankAccounts using the stored or expected platformItemId before retrying. Retry only if Plaid returns a fresh public_token from a new Link session — Plaid public tokens are short-lived and single-use.

Related pages

  • Managing Bank Account Spend Power — read balances and spend power, and trigger a realtime refresh.
  • Relinking Disconnected Bank Accounts — repair a connection that has dropped.
  • Getting Bank Account Transaction Data — read historical bank transactions.