---
title: "Portals & Cross-Workspace Queries"
description: "Manage multiple client portals from one parent workspace. Query banks, feeds, integrations, and jobs across all portals with the scope=family parameter."
section: "API"
canonical: "https://banksync.io/docs/api/portals"
---

If you're an accountant managing multiple clients, BankSync lets you organise each client into a Portal: a dedicated workspace they own, billed to you. This guide explains how to query data across your portals programmatically with the `scope=family` parameter.

## What's a portal?

A portal is a child workspace under your parent workspace. Each portal has its own:

- Bank connections (the client's own, isolated from yours)
- Members (the client themselves, plus anyone they invite if you allow it)
- Feeds, integrations, jobs
- Soft-delete + 30-day cleanup lifecycle

Your portals roll up to your plan: one Stripe subscription pays for the parent and every portal under it. The parent's owner and admins have implicit owner / admin authority on every portal, no separate invitation needed.

> **Who can use cross-workspace queries**
>
> Family-scoped reads require **admin** or **owner** role on the parent workspace. Portal-direct members (clients you've invited) cannot use them: they only see their own portal's data.

## The `scope` query parameter

Every list endpoint accepts an optional `scope` query parameter that controls what set of workspaces the query operates over.

| Value            | Returns                                                                                | Requires                              |
| ---------------- | -------------------------------------------------------------------------------------- | ------------------------------------- |
| `self` (default) | Just this workspace's rows                                                             | The endpoint's standard role          |
| `family`         | This workspace + every live portal under it. Each row gains `workspaceId` and `portal` | **Admin+**, top-level workspaces only |

Endpoints supporting `scope=family` today:

- `GET /v1/banks`
- `GET /v1/feeds`
- `GET /v1/integrations`
- `GET /v1/jobs` (internal API only for now)

## Example: list every bank across all your portals

Your parent-workspace API key works for this: no need to mint a separate key per portal.

```bash
curl -X GET "https://api.banksync.io/v1/banks?scope=family" \
  -H "X-API-Key: bsk_your_api_key_here" \
  -H "Content-Type: application/json"
```

```javascript
const response = await fetch('https://api.banksync.io/v1/banks?scope=family', {
  headers: {
    'X-API-Key': process.env.BANKSYNC_API_KEY,
    'Content-Type': 'application/json',
  },
})
const { data: banks } = await response.json()
for (const bank of banks) {
  if (bank.portal) {
    console.log(`${bank.name} - ${bank.portal.name}`)
  } else {
    console.log(`${bank.name} - (my workspace)`)
  }
}
```

```python
import os
import requests

response = requests.get(
    "https://api.banksync.io/v1/banks?scope=family",
    headers={
        "X-API-Key": os.environ["BANKSYNC_API_KEY"],
        "Content-Type": "application/json",
    },
)
for bank in response.json()["data"]:
    where = bank["portal"]["name"] if bank.get("portal") else "(my workspace)"
    print(f"{bank['name']} - {where}")
```

### Response shape

Each row gains two fields when `scope=family` is requested:

```json
{
  "success": true,
  "data": [
    {
      "id": "bnk_abc",
      "name": "Chase Checking",
      "workspaceId": "ws_parent",
      "portal": null,
      ...
    },
    {
      "id": "bnk_xyz",
      "name": "Acme Co Operating",
      "workspaceId": "ws_acme_portal",
      "portal": {
        "id": "ws_acme_portal",
        "name": "Acme Co",
        "icon": "🏢"
      },
      ...
    }
  ]
}
```

- `workspaceId`: the workspace the row actually lives in. For your own rows this is the parent workspace; for portal rows this is the portal's id.
- `portal`: attribution badge. `null` for rows owned by the parent; a `{ id, name, icon? }` object for rows owned by a portal.

## Targeting a single portal

Portals are workspaces: you can address one directly via its workspace id. Your parent-scoped API key is automatically authorised against every live portal you own; no per-portal key needed.

```bash
# List banks in one specific client's portal
curl -X GET "https://api.banksync.io/v1/banks" \
  -H "X-API-Key: bsk_your_api_key_here" \
  -H "X-Workspace-Id: ws_acme_portal"
# Or use the workspace id in the URL path (depending on the endpoint).
```

The credential still has to come from your parent workspace (or be a Firebase session). A key minted on workspace A cannot reach into workspace B's portals, only into A's own.

## Error responses

| Status | When                                                                                                                 |
| ------ | -------------------------------------------------------------------------------------------------------------------- |
| `400`  | Unknown `scope` value (anything other than `self` / `family`), or `scope=family` was sent against a portal workspace |
| `403`  | Your role on the parent workspace is editor or viewer: `scope=family` requires admin+                                |
| `404`  | The workspace id in the URL doesn't exist or was deleted                                                             |

## Using portals with MCP

Connect an MCP client (Claude Code, Cursor, etc.) to your parent workspace once: your portals appear automatically.

- `list_workspaces` surfaces every portal you manage with `isInherited: true` and a `parentWorkspaceId`.
- All workspace-scoped tools (`list_banks`, `list_feeds`, …) accept any portal's `workspaceId` you own.
- See the [MCP setup guide](/docs/mcp/connect-an-agent) for connecting your client.

> **For clients, not advisors**
>
> If you've been invited into a portal by an accounting firm, programmatic access is intentionally not available: your accountant manages API keys and MCP connections on your behalf. The portal Developer Settings tab is hidden for portal-direct members.

## Internal API vs public REST API

Two surfaces ship the same `scope=family` contract:

- **Public REST API** (`api.banksync.io/v1/*`): the surface most third-party integrations use. Banks, feeds, integrations all accept `?scope=family`.
- **Internal API** (`internal-api.banksync.io/api/*`): used by the BankSync web app and MCP server. Same scope semantics, plus a `/jobs` workspace-aggregate endpoint.

## See also

- [Authentication](/docs/api/authentication): API keys, scopes, and bearer tokens
- [API reference](/docs/api/reference): every endpoint and its response shape
- [MCP setup](/docs/mcp/connect-an-agent): connect Claude Code or other MCP clients
