> ## 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.

# Get Bulk Transactions

> Fetch transactions across your connected users as a single flat, keyset-paginated list.

Returns transactions across your connected users as a **single flat, keyset-paginated list**, newest first. Every row carries the `externalReferenceId` and `accountId` of the user it belongs to. There is **no per-user cap** — page with `nextCursor` to walk every transaction in the window across every target user.

Defaults to the **last 90 days** when no date filter is supplied.

<Note>
  **Breaking change.** This query previously returned `results`, one entry per target user, each holding up to 20 transactions. It now returns a flat `transactions` list with cursor pagination. See [Migrating from the per-target response](#migrating-from-the-per-target-response).
</Note>

## Requirements

* `Authorization: Basic <API_KEY>` (your application API key)
* The **bulk API capability** on your application
* The `LIST_PAYMENT` and `LIST_PURCHASES` scopes on each target user's grant

Failures are reported per target and never fail the whole request. See [Bulk API Overview](/bulk-api) for the targeting model and error codes.

## Query

```graphql theme={null}
query GetBulkTransactions(
  $targetSpec: BulkTargetSpecInput!
  $createdGte: DateTime
  $createdLte: DateTime
  $includeMetadata: Boolean
  $limit: Int
  $after: String
  $includeTotalCount: Boolean
) {
  getBulkTransactions(
    targetSpec: $targetSpec
    createdGte: $createdGte
    createdLte: $createdLte
    includeMetadata: $includeMetadata
    limit: $limit
    after: $after
    includeTotalCount: $includeTotalCount
  ) {
    transactions {
      externalReferenceId
      accountId
      recordId
      transactionType
      amount
      status
      createdAt
      memo
      transactionCategory
    }
    errors {
      externalReferenceId
      accountId
      code
      message
    }
    totalCount
    targetCount
    succeededTargetCount
    failedTargetCount
    nextCursor
    hasMore
  }
}
```

### Variables

```json theme={null}
{
  "targetSpec": { "mode": "SELECTED", "targets": ["your-user-001", "your-user-002"] },
  "createdGte": null,
  "createdLte": null,
  "includeMetadata": false,
  "limit": 100,
  "after": null,
  "includeTotalCount": true
}
```

## Response

```json theme={null}
{
  "data": {
    "getBulkTransactions": {
      "transactions": [
        {
          "externalReferenceId": "your-user-001",
          "accountId": "8f3c…",
          "recordId": "f85b1ce1-1766-4af5-b539-7f3fab4db33d",
          "transactionType": "Account Transfer - In",
          "amount": 25.0,
          "status": "SETTLED",
          "createdAt": "2026-07-01T00:00:00.000Z",
          "memo": null,
          "transactionCategory": null
        },
        {
          "externalReferenceId": "your-user-001",
          "accountId": "8f3c…",
          "recordId": "ede29c85-6d9c-4a7e-b2be-9b6ec7b93cb5",
          "transactionType": "Add Money",
          "amount": 100.0,
          "status": "SETTLED",
          "createdAt": "2026-06-30T18:12:04.000Z",
          "memo": null,
          "transactionCategory": null
        }
      ],
      "errors": [
        {
          "externalReferenceId": "your-user-002",
          "accountId": null,
          "code": "TARGET_NOT_CONNECTED",
          "message": "The id does not correspond to a user currently connected to your application."
        }
      ],
      "totalCount": 1284,
      "targetCount": 2,
      "succeededTargetCount": 1,
      "failedTargetCount": 1,
      "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA2…",
      "hasMore": true
    }
  }
}
```

## Arguments

| Argument            | Type                   | Description                                                                                                                            |
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `targetSpec`        | `BulkTargetSpecInput!` | Target selection. See [Targeting](#targeting).                                                                                         |
| `createdGte`        | `DateTime`             | Only include transactions created at/after this time. Defaults to 90 days before `createdLte`. Must not be after `createdLte`.         |
| `createdLte`        | `DateTime`             | Only include transactions created at/before this time. Defaults to now.                                                                |
| `includeMetadata`   | `Boolean`              | Also return `memo` and `transactionCategory`. Slower; defaults to `false`.                                                             |
| `limit`             | `Int`                  | Maximum transactions in this page. Default and maximum are both `100`. Larger values are clamped, not rejected.                        |
| `after`             | `String`               | Opaque cursor from a previous response's `nextCursor`. Omit for the first page.                                                        |
| `includeTotalCount` | `Boolean`              | Also return `totalCount`. Costs a second scan of the window, so it defaults to `false`. Prefer `hasMore`/`nextCursor` to drive paging. |

The date window bounds the data; `limit` bounds a single page. **The window may span at most 365 days.** Transactions are stored in monthly partitions and each partition in the window is scanned per target, so a wider window costs more per page — it isn't free. For longer history, use the export.

### Targeting

`targetSpec.mode` is either `SELECTED` (name the users in `targets`, using the `externalReferenceId` you connected them with) or `ALL_CONNECTED` (every user with an active grant on your application). Duplicate `targets` are removed.

This query accepts up to **1,000 target users**. For more than that, use the asynchronous export.

## Response fields

| Field                  | Type                    | Description                                                                                                                                                                                                                                                                    |
| ---------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `transactions`         | `[BulkTransaction!]!`   | This page of transactions across all target users, newest first, each attributed to its user.                                                                                                                                                                                  |
| `errors`               | `[BulkTargetFailure!]!` | Targets that were skipped or failed. **First page only** (empty on subsequent pages), and capped at a **sample of 100** so a large sweep can't return a huge payload. Use `failedTargetCount` for the true total.                                                              |
| `totalCount`           | `Int`                   | Total transactions matching the request across **all pages** — every target, whole window. Opt-in via `includeTotalCount`, and only ever computed on the first page; `null` otherwise. It costs a second scan of the window, so ask for it only when you need to show a total. |
| `targetCount`          | `Int!`                  | Number of target users addressed by the request.                                                                                                                                                                                                                               |
| `succeededTargetCount` | `Int!`                  | Number of target users this request was authorized to read — resolved, and holding the required scopes. Independent of this page's contents: an authorized user with no transactions in the window still counts.                                                               |
| `failedTargetCount`    | `Int!`                  | Number of target users that could not be read. Returned on **every** page (unlike `errors`), and the true total even when `errors` is truncated to its 100-item sample.                                                                                                        |
| `nextCursor`           | `String`                | Opaque cursor for the next page; `null` when this is the last page. Keyset-based, so it's stable across concurrent writes.                                                                                                                                                     |
| `hasMore`              | `Boolean!`              | Whether more transactions remain beyond this page.                                                                                                                                                                                                                             |

### `BulkTransaction`

| Field                 | Type                | Description                                                                                                                                                        |
| --------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `externalReferenceId` | `String`            | Your reference id for the user this transaction belongs to.                                                                                                        |
| `accountId`           | `UUID!`             | The Fluz account id of the user this transaction belongs to.                                                                                                       |
| `recordId`            | `UUID`              | Unique identifier for the transaction record.                                                                                                                      |
| `transactionType`     | `String`            | Display-style label, e.g. `"Account Transfer - In"`, `"Account Transfer - Out"`, `"Spend Account Transfer - In"`, `"Add Money"`. Not an enum — see the note below. |
| `amount`              | `Float!`            | The transaction amount.                                                                                                                                            |
| `status`              | `TransactionStatus` | `PENDING` or `SETTLED`.                                                                                                                                            |
| `createdAt`           | `DateTime`          | When the transaction was created.                                                                                                                                  |
| `memo`                | `String`            | Populated only when `includeMetadata: true`.                                                                                                                       |
| `transactionCategory` | `String`            | Populated only when `includeMetadata: true`.                                                                                                                       |

Four behaviors to be aware of:

* **`transactionType` is a human-readable label, not a stable enum** — values look like `"Account Transfer - In"` or `"Add Money"`, not `TRANSFER` or `DEPOSIT`. The set is open and the wording may change. Don't switch on it in code; use `recordId` and the amount/sign for logic, and treat this field as display text.
* **Only `PENDING` and `SETTLED` transactions are returned** — declined and other non-baseline records are excluded, matching the single-user transactions surface.
* **Ordering is newest first** across the whole flat stream (`createdAt` descending, ties broken by `recordId`, then by user).
* **A transaction on a shared account appears once per grant that can see it** — two attributed rows with the same `recordId` and different `externalReferenceId`.

`BulkTransaction` is a purpose-built, narrow type. It is **not** the full `Transaction` type returned by the single-user [transactions query](/features/get-all-transactions) — the fields above are the complete set. For richer per-transaction data, use the asynchronous export.

### `BulkTargetFailure`

| Field                 | Type      | Description                                            |
| --------------------- | --------- | ------------------------------------------------------ |
| `externalReferenceId` | `String`  | Your reference id for the target, when available.      |
| `accountId`           | `UUID`    | The target's account id, when the target was resolved. |
| `code`                | `String!` | Machine-readable failure reason (see below).           |
| `message`             | `String!` | Human-readable failure detail.                         |

| `code`                      | Meaning                                                                  |
| --------------------------- | ------------------------------------------------------------------------ |
| `TARGET_NOT_CONNECTED`      | The id is not a user currently connected to your application.            |
| `INVALID_TARGET_IDENTIFIER` | The id is not a valid identifier.                                        |
| `INSUFFICIENT_SCOPE`        | The user's grant is missing `LIST_PAYMENT` and/or `LIST_PURCHASES`.      |
| `ACCOUNT_NOT_PERMITTED`     | The grant does not permit this operation on the requested spend account. |

A failed target never fails the request.

**Select both identifiers.** `externalReferenceId` is `null` for any grant you connected without one, so on its own it may not tell you which user failed. `accountId` is populated whenever the target resolved — which is the case for `INSUFFICIENT_SCOPE` and `ACCOUNT_NOT_PERMITTED` — and is `null` only for `TARGET_NOT_CONNECTED` and `INVALID_TARGET_IDENTIFIER`, where nothing resolved. Querying `errors { externalReferenceId accountId code message }` means every entry is identifiable by at least one of the two.

### Request-level errors

Everything above is per-target. These reject the whole request, returning `data.getBulkTransactions: null` plus a GraphQL error. **Branch on `extensions.code`, never on the message text** — wording can change, codes won't.

| Condition                                                              | `extensions.code`   | `extensions.errorName`    | `statusCode` |
| ---------------------------------------------------------------------- | ------------------- | ------------------------- | ------------ |
| Window wider than 365 days                                             | `APPLICATIONS-0009` | `InvalidDateWindow`       | 422          |
| `createdGte` after `createdLte`                                        | `APPLICATIONS-0009` | `InvalidDateWindow`       | 422          |
| Malformed or tampered `after` cursor                                   | `APPLICATIONS-0010` | `InvalidPaginationCursor` | 422          |
| No eligible targets — none connected, or none with the required scopes | `APPLICATIONS-0011` | `InvalidBulkRequest`      | 422          |
| Malformed request payload (bad `targetSpec`, empty `targets`)          | `APPLICATIONS-0011` | `InvalidBulkRequest`      | 422          |
| More than 1,000 target users                                           | `APPLICATIONS-0008` | `TargetLimitExceeded`     | 422          |
| Bulk API not enabled for your application                              | `APPLICATIONS-0003` | `BulkApiAccessDenied`     | 403          |
| Missing credentials, or a non-Basic scheme                             | `AUTH-0002`         | `InvalidCredentials`      | 401          |

The two date failures share a code — both mean "the range you asked for isn't usable" — and the `message` distinguishes them:

```json theme={null}
{
  "errors": [
    {
      "message": "The requested window exceeds the 365-day maximum. Narrow the range, or use the asynchronous transactions export for a wider pull.",
      "path": ["getBulkTransactions"],
      "extensions": {
        "errorName": "InvalidDateWindow",
        "code": "APPLICATIONS-0009",
        "statusCode": 422,
        "userFriendly": true
      }
    }
  ],
  "data": { "getBulkTransactions": null }
}
```

A cursor altered in any way — truncated, re-encoded, hand-edited — fails as `APPLICATIONS-0010` before it reaches the database. Treat it as "start again from page one", not as retryable.

## Paging

Read the first page, then follow `nextCursor` until `hasMore` is `false`. Keep every other argument identical across pages — the cursor encodes a position in that specific query's ordering.

```graphql theme={null}
# Page 1: omit `after`. Read errors here — they are first-page only. Ask for totalCount only
# if you actually need it; it costs an extra scan.
{
  getBulkTransactions(targetSpec: { mode: ALL_CONNECTED }, limit: 100, includeTotalCount: true) {
    transactions { recordId externalReferenceId amount createdAt }
    errors { externalReferenceId accountId code message }
    totalCount
    failedTargetCount
    nextCursor
    hasMore
  }
}

# Page 2+: pass the previous nextCursor.
{
  getBulkTransactions(
    targetSpec: { mode: ALL_CONNECTED }
    limit: 100
    after: "eyJjcmVhdGVkQXQiOiIyMDI2LTA2…"
  ) {
    transactions { recordId externalReferenceId amount createdAt }
    nextCursor
    hasMore
  }
}
```

Because paging is keyset-based rather than offset-based, transactions written while you page won't shift rows across page boundaries or produce duplicates.

## Migrating from the per-target response

| Before                                        | Now                                                                                                                     |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `results[]`, one entry per target user        | `transactions[]`, one entry per transaction. Group client-side by `externalReferenceId` to rebuild the old shape.       |
| `results[].transactions` (full `Transaction`) | Top-level `transactions` of the narrower `BulkTransaction` type.                                                        |
| `results[].error`                             | `errors[]` — a flat list of failed targets, first page only, capped at a 100-item sample.                               |
| `results[].totalCount` (per target)           | `totalCount` (request-wide, opt-in via `includeTotalCount`, first page only). Per-target totals are no longer returned. |
| `results[].hasNextPage` (per target)          | `hasMore` + `nextCursor` (request-wide keyset paging).                                                                  |
| `successCount` / `failureCount`               | `succeededTargetCount` / `failedTargetCount`.                                                                           |
| 20 transactions per target, hard cap          | No per-user cap. `limit` (default and max 100) bounds the page.                                                         |
| 90-day window was a hard cap                  | 90 days is now the default; the hard cap is 365 days.                                                                   |
| 100 target users, hard cap                    | 1,000 target users.                                                                                                     |

## When to use the export instead

`getBulkTransactions` can walk complete history by paging, so the export is for pulls you'd rather not page through — more than 1,000 users, windows longer than a year, scheduled batch jobs, or when you need richer per-transaction fields than `BulkTransaction` carries. Submit `GET_TRANSACTIONS_EXPORT` via `submitBulkOperation`, poll `getBulkOperationJob`, and download the NDJSON from the short-lived `resultUrl`. See [Bulk Operations](/bulk-api).
