> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fluz.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Linking Bank Account via Plaid

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:

```http theme={null}
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

```graphql theme={null}
mutation CreatePlaidLinkToken($input: CreatePlaidLinkTokenInput!) {
  createPlaidLinkToken(input: $input) {
    linkToken
    expiration
    requestId
    mode
  }
}
```

```json theme={null}
{
  "input": {}
}
```

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

```json theme={null}
{
  "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.

```graphql theme={null}
mutation CompletePlaidLink($input: CompletePlaidLinkInput!) {
  completePlaidLink(input: $input) {
    requiresAddress
    bankAccountId
    bankInstitutionAuthId
    newlyLinkedBankInstitutionAuthId
    bankInstitutionName
    platformItemId
    bankAccounts {
      bankInstitutionAuthId
      bankAccountId
      bankName
      lastFour
      type
      subtype
    }
  }
}
```

```json theme={null}
{
  "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.

```html theme={null}
<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*).

```javascript theme={null}
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.

```graphql theme={null}
query GetPlaidBankAccounts($input: PlaidBankAccountFilterInput) {
  getPlaidBankAccounts(input: $input) {
    platformItemId
    bankInstitutionAuthId
    bankInstitutionName
    bankAccountId
    accountName
    lastFour
    type
    subtype
    status
  }
}
```

```json theme={null}
{
  "input": {
    "platformItemId": "plaid-item-id"
  }
}
```

## Attach an address

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

```graphql theme={null}
mutation CreatePlaidLinkAddress($input: CreatePlaidLinkAddressInput!) {
  createPlaidLinkAddress(input: $input) {
    addressId
  }
}
```

```json theme={null}
{
  "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.

```graphql theme={null}
mutation RemovePlaidBankInstitution($input: RemovePlaidBankInstitutionInput!) {
  removePlaidBankInstitution(input: $input) {
    removed
    bankInstitutionAuthId
  }
}
```

```json theme={null}
{
  "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.

<br />
