Portals & Cross-Workspace Queries

Manage multiple client portals from one parent workspace. Query banks, feeds, integrations, and jobs across all portals with the scope=family parameter.

4 min read

On this page

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.

ValueReturnsRequires
self (default)Just this workspace's rowsThe endpoint's standard role
familyThis workspace + every live portal under it. Each row gains workspaceId and portalAdmin+, 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.

Shell
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 osimport 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.

Shell
# List banks in one specific client's portalcurl -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#

StatusWhen
400Unknown scope value (anything other than self / family), or scope=family was sent against a portal workspace
403Your role on the parent workspace is editor or viewer: scope=family requires admin+
404The 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 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#

Use this page with your AI assistant

Every BankSync doc is available as plain Markdown for agents and LLMs.