# Virtual Accounts

A **virtual account** is an issued IBAN that lets customers pay a seller by bank transfer without exposing the seller's primary bank account.

> [!CAUTION]
> **The `virtualAccounts` and `virtualAccount` queries are not exposed on `/dashboard/graphql`.** They exist on the Fynex staff surface only, backed by the banking-provider integration; a `dashboard_session` calling them receives `Cannot query field "virtualAccounts" on type "Query"`. They are documented below for reference — ask your Fynex representative for account listings until a seller-facing read ships. The REST issuance preview described below creates a different account record and is not returned by those queries either.

---

## When to use virtual accounts

- **Collect bank-transfer payments** without exposing the seller's primary IBAN.
- **Isolate funds per customer** — issue one virtual account per customer and reconcile inflows by IBAN.
- **Reconcile provider account activity** using the provider-specific account surface enabled for the seller.

---

## Provider boundaries

- The GraphQL operations below list and inspect banking-provider-backed virtual accounts.
- `POST /api/v1/accounts/virtual` is a tier-gated issuance preview. It currently returns only an internal Fynex UUID.
- There is no seller-facing read/status endpoint for issued accounts in this API version, and the GraphQL operations below cannot resolve the UUID returned by REST issuance.
- Provider settlement events update the issued account/payment records, but they do not currently create seller wallet-ledger entries. Do not use wallet polling as confirmation of an incoming transfer.

Use the issuance preview only as part of a coordinated rollout with Fynex. Do not present its returned UUID as an IBAN or build automated account-status or incoming-funds reconciliation until the corresponding seller read and wallet-ledger surfaces are released.

---

## GraphQL operations

> [!CAUTION]
> Staff surface only — not callable with a `dashboard_session`. Shown for reference.

| Operation | Signature |
|-----------|-----------|
| List virtual accounts | `virtualAccounts(first: Int, after: String, type: VirtualAccountType): VirtualAccountConnection!` |
| Get a single account | `virtualAccount(id: ID!): VirtualAccount` |

### `VirtualAccountType` enum

| Value | Description |
|-------|-------------|
| `CLIENT` | Account issued for a specific customer / payer |
| `OPERATING` | Account used for the seller's own operating funds |

---

## Type reference

### `VirtualAccount`

| Field | Type | Description |
|-------|------|-------------|
| `id` | `ID!` | Internal Fynex account ID |
| `accountId` | `String!` | The issued IBAN or account number to share with the payer |
| `virtualAccountId` | `String!` | Banking partner's own reference for this account |
| `currency` | `Currency!` | Account currency |
| `type` | `VirtualAccountType!` | `CLIENT` or `OPERATING` |
| `status` | `String!` | Current provisioning status |
| `balance` | `Float!` | Current balance (in major currency units) |
| `createdAt` | `String!` | ISO timestamp of account creation |
| `customerInfo` | `VirtualAccountCustomer` | Optional customer metadata (name, email, phone, address) |

### `VirtualAccountConnection`

Cursor-based pagination is used for `virtualAccounts`:

| Field | Type | Description |
|-------|------|-------------|
| `edges` | `[VirtualAccountEdge!]!` | Paginated account nodes |
| `pageInfo` | `PageInfo!` | Cursor and page metadata |

`PageInfo` fields: `hasNextPage`, `hasPreviousPage`, `startCursor`, `endCursor`, `totalCount`, `currentPage`, `totalPages`.

Advance pages by passing the `endCursor` value as the `after` argument on the next call.

---

## Requesting a virtual account

> [!CAUTION]
> **Tier-gated preview.** `POST /api/v1/accounts/virtual` may not be enabled for all seller tiers. It does not yet provide a public status/read flow. Confirm the rollout and reconciliation process with Fynex before using it.

```http
POST /api/v1/accounts/virtual
Content-Type: application/json
Authorization: Bearer <seller-api-token>
Idempotency-Key: va_customer-jane_2026-08-08

{
	"name": "Jane Smith"
}
```

The seller is derived from the bearer token; the request does not accept a merchant or seller ID. A successful request returns `201 Created`:

```json
{
	"accountId": "0f834fa8-f5ab-4d80-96b1-fc2d9e4e1824"
}
```

`accountId` is the internal Fynex UUID for the issuance record, not the issued IBAN. Retrying the same request with the same `Idempotency-Key` returns the same UUID without issuing another account. Reusing that key with a different name is rejected. Keep the key stable until the request has a definitive response. The dashboard GraphQL queries continue to use the dashboard cookie, but they do not expose this issuance record; REST issuance uses seller bearer authentication.

---

## Code samples — list virtual accounts

#### curl

```bash
# Step 1 — login
curl -sc cookies.txt \
  -X POST https://api.fynex.ai/api/v1/login/dashboard \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "your_password"}'

# Step 2 — list virtual accounts (first 20, CLIENT type)
curl -b cookies.txt \
  -X POST https://api.fynex.ai/dashboard/graphql \
  -H "Content-Type: application/json" \
  -d '{
    "query": "# Not available on /dashboard/graphql — staff surface only\nquery ListVAs($first: Int, $after: String, $type: VirtualAccountType) { virtualAccounts(first: $first, after: $after, type: $type) { pageInfo { hasNextPage endCursor totalCount } edges { cursor node { id accountId virtualAccountId currency type status balance createdAt customerInfo { name email } } } } }",
    "variables": { "first": 20, "type": "CLIENT" }
  }'
```

#### JavaScript

```js
const BASE = 'https://api.fynex.ai';

async function login(email, password) {
  const res = await fetch(`${BASE}/api/v1/login/dashboard`, {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  if (!res.ok) throw new Error(`Login failed: ${res.status}`);
}

async function gql(query, variables = {}) {
  const res = await fetch(`${BASE}/dashboard/graphql`, {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  });
  const { data, errors } = await res.json();
  if (errors?.length) throw new Error(errors[0].message);
  return data;
}

await login('you@example.com', 'your_password');

// Fetch all CLIENT-type virtual accounts using cursor pagination
let after = undefined;
let allAccounts = [];

do {
  const { virtualAccounts } = await gql(
    `# Not available on /dashboard/graphql — staff surface only
     query ListVAs($first: Int, $after: String, $type: VirtualAccountType) {
       virtualAccounts(first: $first, after: $after, type: $type) {
         pageInfo {
           hasNextPage
           endCursor
           totalCount
         }
         edges {
           node {
             id
             accountId
             virtualAccountId
             currency
             type
             status
             balance
             createdAt
             customerInfo {
               name
               email
             }
           }
         }
       }
     }`,
    { first: 20, after, type: 'CLIENT' }
  );

  allAccounts = allAccounts.concat(virtualAccounts.edges.map((e) => e.node));
  after = virtualAccounts.pageInfo.hasNextPage ? virtualAccounts.pageInfo.endCursor : undefined;
} while (after);

console.log(`Fetched ${allAccounts.length} virtual accounts`);
for (const va of allAccounts) {
  console.log(`${va.accountId} (${va.currency}) — status: ${va.status}, balance: ${va.balance}`);
}
```

#### Python

```python
import requests

BASE = "https://api.fynex.ai"
session = requests.Session()

session.post(
    f"{BASE}/api/v1/login/dashboard",
    json={"email": "you@example.com", "password": "your_password"},
).raise_for_status()

query = """
  # Not available on /dashboard/graphql — staff surface only
  query ListVAs($first: Int, $after: String, $type: VirtualAccountType) {
    virtualAccounts(first: $first, after: $after, type: $type) {
      pageInfo {
        hasNextPage
        endCursor
        totalCount
      }
      edges {
        node {
          id
          accountId
          virtualAccountId
          currency
          type
          status
          balance
          createdAt
          customerInfo {
            name
            email
          }
        }
      }
    }
  }
"""

all_accounts = []
after = None

while True:
    variables = {"first": 20, "type": "CLIENT"}
    if after:
        variables["after"] = after

    resp = session.post(
        f"{BASE}/dashboard/graphql",
        json={"query": query, "variables": variables},
    )
    resp.raise_for_status()
    result = resp.json()
    if "errors" in result:
        raise RuntimeError(result["errors"][0]["message"])

    page = result["data"]["virtualAccounts"]
    all_accounts.extend(edge["node"] for edge in page["edges"])

    if page["pageInfo"]["hasNextPage"]:
        after = page["pageInfo"]["endCursor"]
    else:
        break

print(f"Total virtual accounts: {len(all_accounts)}")
for va in all_accounts:
    print(f"{va['accountId']} ({va['currency']}) — {va['status']}, balance: {va['balance']}")
```

## See also

- **[GraphQL Authentication](https://api.fynex.ai/payments-api/v2/docs#tag/graphql-auth)** — Obtain the dashboard_session cookie required for all GraphQL calls.
- **[Wallets](https://api.fynex.ai/payments-api/v2/docs#tag/wallets)** — Inspect wallet-ledger activity for products that post funds to the seller wallet. The issuance preview does not currently create those entries.
- **[Concepts](https://api.fynex.ai/payments-api/v2/docs#tag/concepts)** — The object model behind virtual accounts: sellers, payees, wallets and how funds move between them.
