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

# Add Expense Details

You can enrich any transaction with a **memo**, a **category**, and/or an **attachment** (e.g. receipts, invoices, purchase orders). Annotations can be added at the time of the original transaction or updated afterward using the `updateTransactionMetadata` mutation.

For accounts with ERP integrations enabled and an active QuickBooks Online connection, you can also manage ERP transaction metadata: accounting category, vendor, customer, billable status, and an ERP-specific memo. ERP metadata is separate from annotations.

Annotations are supported on:

* Deposits (`depositCashBalance`)
* Gift card purchases (`purchaseGiftCard`)
* Wallet transfers (`createTransfer`, `transferInternalBalance`)
* Virtual card creation (`createVirtualCard`)
  * `attachment` is excluded

ERP metadata is supported on:

* Existing transactions (`updateTransactionMetadata.input.erpTransactionMetadata`)
* Bulk transaction updates (`bulkUpdateErpTransactionMetadata`)
* Transaction reads (`Transaction.erpMetadata`, `erpTransactionMetadata`, `erpTransactionMetadataList`)

***

## How it works

1. **Optional:** If you want to attach a file, upload it first via the REST upload endpoint. You'll get back an `attachmentId`.
2. Pass `memo`, `transactionCategory`, and/or `attachmentId` into your mutation input — either at transaction time or later via `updateTransactionMetadata`.
3. For ERP metadata, first query imported QuickBooks Online reference items, or pass names that should be resolved or created in QuickBooks Online.
4. Update ERP metadata on one transaction with `updateTransactionMetadata.input.erpTransactionMetadata`, or update up to 100 transactions with `bulkUpdateErpTransactionMetadata`.
5. Read annotations back on `getTransactions`, `getUserPurchases`, or the mutation response. Read ERP details on `Transaction.erpMetadata`, `erpTransactionMetadata`, or `erpTransactionMetadataList`. The `attachmentUrl` field returns a short-lived signed URL for file access.

***

## Step 1: Upload an attachment (optional)

> This is a REST endpoint, not a GraphQL mutation

### Endpoint

`POST /api/v1/file-upload/transaction-memo-attachment`

### Authentication

`Authorization: Bearer <YOUR_USER_ACCESS_TOKEN>`

### Request

Send the file as `multipart/form-data` with the field name `file`.

**Accepted types:** `application/pdf`, `image/png`

> ⚠️ JPEG is not accepted for transaction attachments.

### Sample Request

```bash theme={null}
curl -X POST https://transactional-graph.staging.fluzapp.com/api/v1/file-upload/transaction-memo-attachment \
  -H "Authorization: Bearer <YOUR_ACCESS_TOKEN>" \
  -F "file=@receipt.pdf"
```

### Response

```json theme={null}
{
  "attachmentId": "3f2a1b4c-e5d6-7890-abcd-ef1234567890"
}
```

Copy the `attachmentId` — you'll pass it into your mutation input in the next step.

> The `attachmentId` is scoped to your account. The system verifies the file exists in your account's storage when you submit the mutation. An ID from a different account will be rejected.

### Upload Errors

| Cause                             | Status | Details             |
| --------------------------------- | ------ | ------------------- |
| No file included in request       | 400    | Missing file        |
| File type not allowed (e.g. JPEG) | 400    | `INVALID_ARGUMENTS` |

***

## Step 2: Annotate the transaction

You can provide annotations at the time of the original transaction **or** update them afterward.

### Option A — At transaction time

The following mutations accept `memo`, `transactionCategory`, and `attachmentId` as optional fields in their input:

* `depositCashBalance` → `DepositCashBalanceInput`
* `purchaseGiftCard` → `PurchaseGiftCardInput`
* `createTransfer` → `CreateTransferInput`
* `transferInternalBalance` → `TransferInternalBalanceInput`

#### Annotation Fields

| Field                 | Type   | Description                                                                                                                |
| --------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| `memo`                | String | Free-text note. Max 255 characters.                                                                                        |
| `transactionCategory` | String | Category label. Free-form — categories are created automatically on first use and reused if the same name is passed again. |
| `attachmentId`        | String | The ID returned by the upload endpoint. The file must be uploaded before submitting the mutation.                          |

#### Sample — Purchase Gift Card with Annotation

```graphql theme={null}
mutation purchaseGiftCard($input: PurchaseGiftCardInput!) {
  purchaseGiftCard(input: $input) {
    purchaseDisplayId
    purchaseAmount
    memo
    transactionCategory
    attachmentUrl
    giftCard {
      giftCardId
      status
    }
  }
}
```

```json theme={null}
{
  "idempotencyKey": "0284be6f-1a69-44f7-9da0-5b5edaf45d19",
  "offerId": "0284be6f-1a69-44f7-9da0-5b5edaf45d19",
  "amount": 50.0,
  "balanceAmount": 50.0,
  "memo": "Team lunch — Q2",
  "transactionCategory": "Meals & Entertainment",
  "attachmentId": "3f2a1b4c-e5d6-7890-abcd-ef1234567890"
}
```

***

### Option B — After the transaction (`updateTransactionMetadata`)

Use this mutation to add or update annotations on any existing transaction.

> **Partial update semantics:** Only the fields you include are updated. Omitted fields are left unchanged. Pass `null` to clear a field.

#### Mutation

```graphql theme={null}
mutation updateTransactionMetadata($input: UpdateTransactionMetadataInput!) {
  updateTransactionMetadata(input: $input) {
    recordId
    memo
    transactionCategory
    attachmentUrl
  }
}
```

#### UpdateTransactionMetadataInput

| Field                    | Type                                | Required | Description                                                                               |
| ------------------------ | ----------------------------------- | -------- | ----------------------------------------------------------------------------------------- |
| `recordId`               | UUID!                               | Yes      | The `recordId` of the transaction to update.                                              |
| `memo`                   | String                              | No       | Free-text note. Max 255 characters. Omit to leave unchanged; pass `null` to clear.        |
| `transactionCategory`    | String                              | No       | Category label. Omit to leave unchanged; pass `null` to clear.                            |
| `attachmentId`           | String                              | No       | ID from the upload endpoint. Omit to leave unchanged; pass `null` to clear.               |
| `erpTransactionMetadata` | `UpdateErpTransactionMetadataInput` | No       | Optional ERP-aware categorization. Omit, or pass `null`, to leave ERP metadata unchanged. |

#### Required scopes

`LIST_PAYMENT` and `LIST_PURCHASES`. If you include `erpTransactionMetadata`, the request also requires `MANAGE_ERP_TRANSACTION_METADATA`.

#### Sample — Add a memo and category

```json theme={null}
{
  "recordId": "550e8400-e29b-41d4-a716-446655440000",
  "memo": "Q1 vendor payment",
  "transactionCategory": "Operating Expenses"
}
```

#### Sample — Attach a file to an existing transaction

```json theme={null}
{
  "recordId": "550e8400-e29b-41d4-a716-446655440000",
  "attachmentId": "3f2a1b4c-e5d6-7890-abcd-ef1234567890"
}
```

#### Sample — Clear a memo

```json theme={null}
{
  "recordId": "550e8400-e29b-41d4-a716-446655440000",
  "memo": null
}
```

#### Sample Response

```json theme={null}
{
  "data": {
    "updateTransactionMetadata": {
      "recordId": "550e8400-e29b-41d4-a716-446655440000",
      "memo": "Q1 vendor payment",
      "transactionCategory": "Operating Expenses",
      "attachmentUrl": "https://storage.googleapis.com/..."
    }
  }
}
```

#### Errors

| Cause                                                      | Error                                                                          |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `recordId` not found or belongs to a different account     | `INVALID_ARGUMENTS` — transaction not found                                    |
| Transaction type does not support metadata                 | `INVALID_ARGUMENTS` — transaction does not support metadata                    |
| `attachmentId` is not a valid UUID                         | `INVALID_ARGUMENTS` — Invalid attachment ID                                    |
| File not found in storage (not uploaded, or wrong account) | `INVALID_ARGUMENTS` — Attachment file not found. Please upload the file first. |

***

## Step 3: Manage ERP metadata

ERP metadata is used to categorize transactions before exporting them to the connected accounting provider. The currently available provider is QuickBooks Online.

ERP metadata fields are separate from annotations:

* Annotation `memo` is limited to 255 characters.
* ERP `erpTransactionMetadata.memo` is limited to 4000 characters.
* Annotation `transactionCategory` is a Fluz category label.
* ERP `categoryReferenceItemId` points to a QuickBooks Online chart-of-accounts item.

### UpdateErpTransactionMetadataInput

| Field                     | Type      | Description                                                                                                                                                      |
| ------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `categoryReferenceItemId` | `UUID`    | QuickBooks Online chart-of-accounts reference item ID. Omit to leave unchanged; pass `null` to clear. Must refer to an active chart-of-accounts item.            |
| `categoryName`            | `String`  | Find or create a QuickBooks Online Expense account by name, then use it as the category. Ignored when `categoryReferenceItemId` is supplied. Max 100 characters. |
| `vendorReferenceItemId`   | `UUID`    | QuickBooks Online vendor reference item ID. Omit to leave unchanged; pass `null` to clear. Must refer to an active vendor.                                       |
| `vendorName`              | `String`  | Find or create a QuickBooks Online vendor by name. Ignored when `vendorReferenceItemId` is supplied. Max 100 characters.                                         |
| `customerReferenceItemId` | `UUID`    | QuickBooks Online customer reference item ID. Omit to leave unchanged; pass `null` to clear. Must refer to an active customer.                                   |
| `customerName`            | `String`  | Find or create a QuickBooks Online customer by name. Ignored when `customerReferenceItemId` is supplied. Max 100 characters.                                     |
| `isBillable`              | `Boolean` | Sets billable status. Omit to leave unchanged; pass `null` to clear. If `true`, a customer is required before the transaction can become `READY`.                |
| `memo`                    | `String`  | ERP memo. Omit to leave unchanged; pass `null` to clear. Max 4000 characters.                                                                                    |

Leave `erpTransactionMetadata` out of the request when you do not want to update ERP metadata.

### Sample: Update annotations and ERP metadata together

```graphql theme={null}
mutation updateTransactionMetadata($input: UpdateTransactionMetadataInput!) {
  updateTransactionMetadata(input: $input) {
    recordId
    memo
    transactionCategory
    erpMetadata {
      category {
        referenceItemId
        name
      }
      vendor {
        referenceItemId
        name
      }
      customer {
        referenceItemId
        name
      }
      isBillable
      memo
      syncStatus
    }
  }
}
```

```json theme={null}
{
  "input": {
    "recordId": "550e8400-e29b-41d4-a716-446655440000",
    "memo": "Receipt added in Fluz",
    "transactionCategory": "Office Supplies",
    "erpTransactionMetadata": {
      "categoryReferenceItemId": "11111111-1111-4111-8111-111111111111",
      "vendorName": "Example Vendor",
      "customerReferenceItemId": "22222222-2222-4222-8222-222222222222",
      "isBillable": true,
      "memo": "QuickBooks Online memo for client office supplies"
    }
  }
}
```

***

## Step 4: Find ERP reference items

Use these queries to find imported QuickBooks Online reference items before setting `categoryReferenceItemId`, `vendorReferenceItemId`, or `customerReferenceItemId`.

| Query                | Returns                                             |
| -------------------- | --------------------------------------------------- |
| `erpChartOfAccounts` | Imported QuickBooks Online chart-of-accounts items. |
| `erpVendors`         | Imported QuickBooks Online vendors.                 |
| `erpCustomers`       | Imported QuickBooks Online customers.               |

### Reference item fields

| Field                | Description                                                                           |
| -------------------- | ------------------------------------------------------------------------------------- |
| `referenceItemId`    | Fluz ID for this imported ERP reference item. Use this in ERP metadata update inputs. |
| `externalId`         | QuickBooks Online ID.                                                                 |
| `name`               | Display name.                                                                         |
| `fullyQualifiedName` | Fully qualified provider name, when available.                                        |
| `accountType`        | Chart-of-accounts account type. Only populated for chart-of-accounts items.           |
| `accountSubType`     | Chart-of-accounts subtype. Only populated for chart-of-accounts items.                |
| `active`             | Whether the imported provider item is active.                                         |
| `lastChangedAt`      | Last QuickBooks Online update timestamp, when available.                              |

### Reference item filters

| Field            | Type                      | Description                                                                           |
| ---------------- | ------------------------- | ------------------------------------------------------------------------------------- |
| `q`              | `String`                  | Case-insensitive substring search on name.                                            |
| `active`         | `Boolean`                 | Defaults to `true`. Set `false` to list inactive items.                               |
| `accountType`    | `[String!]`               | Chart-of-accounts only. Filters by QuickBooks Online account type, such as `Expense`. |
| `accountSubType` | `[String!]`               | Chart-of-accounts only. Filters by QuickBooks Online account subtype.                 |
| `sortKey`        | `ErpReferenceItemSortKey` | `NAME`, `FULLY_QUALIFIED_NAME`, or `LAST_CHANGED_AT`. Defaults to `NAME`.             |
| `sortOrder`      | `SortOrder`               | `ASC` or `DESC`. Defaults to `ASC`.                                                   |

### Sample: Search chart of accounts

```graphql theme={null}
query erpChartOfAccounts {
  erpChartOfAccounts(
    filter: {
      q: "office"
      accountType: ["Expense"]
      sortKey: NAME
      sortOrder: ASC
    }
    paginate: { limit: 10, offset: 0 }
  ) {
    totalCount
    hasNextPage
    items {
      referenceItemId
      externalId
      name
      fullyQualifiedName
      accountType
      accountSubType
      active
      lastChangedAt
    }
  }
}
```

### Sample: Search vendors and customers

```graphql theme={null}
query erpCounterparties {
  erpVendors(
    filter: { q: "example vendor" }
    paginate: { limit: 10, offset: 0 }
  ) {
    items {
      referenceItemId
      name
      active
    }
  }
  erpCustomers(
    filter: { q: "example company" }
    paginate: { limit: 10, offset: 0 }
  ) {
    items {
      referenceItemId
      name
      active
    }
  }
}
```

***

## Step 5: Read ERP metadata

ERP metadata can be read from the transaction object or through dedicated ERP metadata queries.

### Read ERP metadata in getTransactions

```graphql theme={null}
query getTransactionsWithErpMetadata {
  getTransactions(paginate: { limit: 10, offset: 0 }) {
    totalCount
    hasNextPage
    transactions {
      recordId
      amount
      memo
      transactionCategory
      erpMetadata {
        transactionMetadataId
        transactionRecordId
        category {
          referenceItemId
          name
        }
        vendor {
          referenceItemId
          name
        }
        customer {
          referenceItemId
          name
        }
        isBillable
        memo
        syncStatus
        isSyncing
        updatedAt
      }
    }
  }
}
```

`erpMetadata` is `null` when the account has no active ERP connection or the transaction has no ERP metadata.

### Read one transaction's ERP metadata

```graphql theme={null}
query erpTransactionMetadata($transactionRecordId: UUID!) {
  erpTransactionMetadata(transactionRecordId: $transactionRecordId) {
    transactionMetadataId
    transactionRecordId
    category {
      referenceItemId
      name
    }
    vendor {
      referenceItemId
      name
    }
    customer {
      referenceItemId
      name
    }
    isBillable
    memo
    syncStatus
    isSyncing
    updatedAt
  }
}
```

```json theme={null}
{
  "transactionRecordId": "550e8400-e29b-41d4-a716-446655440000"
}
```

This query returns `null`, not an error, when there is no ERP metadata for the transaction or the account has no active ERP connection.

### List ERP metadata records

```graphql theme={null}
query erpTransactionMetadataList {
  erpTransactionMetadataList(
    filter: { syncStatus: DRAFT, updatedAfter: "2026-07-01T00:00:00.000Z" }
    paginate: { limit: 20, offset: 0 }
  ) {
    totalCount
    hasNextPage
    items {
      transactionRecordId
      category {
        referenceItemId
        name
      }
      isBillable
      memo
      syncStatus
      updatedAt
    }
  }
}
```

### ERP metadata list filters

| Field                  | Type                            | Description                                       |
| ---------------------- | ------------------------------- | ------------------------------------------------- |
| `syncStatus`           | `ErpTransactionSyncStatus`      | Filter by sync status.                            |
| `updatedAfter`         | `DateTime`                      | Return metadata updated after this timestamp.     |
| `transactionRecordIds` | `[UUID!]`                       | Limit results to specific transaction record IDs. |
| `sortKey`              | `ErpTransactionMetadataSortKey` | Currently `UPDATED_AT`. Defaults to `UPDATED_AT`. |
| `sortOrder`            | `SortOrder`                     | `ASC` or `DESC`. Defaults to `DESC`.              |

***

## ERP sync status

| Status         | Meaning                                                                                                                                                 |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DRAFT`        | Missing required ERP fields and cannot be exported yet. Most transaction types need a category. If `isBillable` is `true`, a customer is also required. |
| `READY`        | Required ERP fields are present and the transaction is ready to export.                                                                                 |
| `SYNC_STARTED` | Export to the accounting provider is in progress. The transaction cannot be edited while this status is active.                                         |
| `SYNCED`       | Export succeeded. The transaction cannot be modified through ERP metadata APIs after this point.                                                        |
| `SYNC_FAILED`  | The last export attempt failed. Edit the metadata and retry export.                                                                                     |

***

## Bulk update ERP metadata

Use `bulkUpdateErpTransactionMetadata` to update ERP metadata for up to 100 transactions in one request.

Each item uses the same `UpdateErpTransactionMetadataInput` fields. Omitted fields are left unchanged; nullable reference fields, `memo`, and `isBillable` can be passed as `null` to clear them.

### Mutation

```graphql theme={null}
mutation bulkUpdateErpTransactionMetadata(
  $items: [BulkUpdateErpTransactionMetadataItem!]!
) {
  bulkUpdateErpTransactionMetadata(items: $items) {
    succeeded
    failed {
      transactionRecordId
      error {
        code
        message
      }
    }
  }
}
```

### Variables

```json theme={null}
{
  "items": [
    {
      "transactionRecordId": "550e8400-e29b-41d4-a716-446655440000",
      "erpTransactionMetadata": {
        "categoryName": "Office Supplies",
        "vendorName": "Example Vendor",
        "memo": "Bulk-updated QuickBooks Online memo"
      }
    },
    {
      "transactionRecordId": "660e8400-e29b-41d4-a716-446655440000",
      "erpTransactionMetadata": {
        "categoryReferenceItemId": "11111111-1111-4111-8111-111111111111",
        "customerName": "Example Company LLC",
        "isBillable": true
      }
    }
  ]
}
```

### Sample response

```json theme={null}
{
  "data": {
    "bulkUpdateErpTransactionMetadata": {
      "succeeded": ["550e8400-e29b-41d4-a716-446655440000"],
      "failed": [
        {
          "transactionRecordId": "660e8400-e29b-41d4-a716-446655440000",
          "error": {
            "code": "ERP-0019",
            "message": "The reference item type does not match the expected type for this field."
          }
        }
      ]
    }
  }
}
```

Per-item ERP validation failures are returned in `failed`, other valid items can still succeed.

***

## Reading annotations

Annotations are returned on the following:

| Query / Mutation             | Type                               | Fields                                                                 |
| ---------------------------- | ---------------------------------- | ---------------------------------------------------------------------- |
| `getTransactions`            | `Transaction`                      | `memo`, `transactionCategory`, `attachmentUrl`, optional `erpMetadata` |
| `getUserPurchases`           | `UserPurchase`                     | `memo`, `transactionCategory`, `attachmentUrl`                         |
| `updateTransactionMetadata`  | `Transaction`                      | `memo`, `transactionCategory`, `attachmentUrl`, optional `erpMetadata` |
| `depositCashBalance`         | `CashBalanceDeposit`               | `attachmentUrl`                                                        |
| `erpTransactionMetadata`     | `ErpTransactionMetadata`           | ERP category, vendor, customer, billable status, ERP memo, sync status |
| `erpTransactionMetadataList` | `ErpTransactionMetadataConnection` | Paginated ERP metadata records                                         |

> ⚠️ **`attachmentUrl` is a signed URL.** It expires shortly after being generated. Do not store it — re-fetch the transaction when you need to display or access the file.

***

## Common errors

| Cause                                                      | Error                                     |
| ---------------------------------------------------------- | ----------------------------------------- |
| Missing annotation scopes                                  | `AUTH-0031` / invalid scope               |
| Missing `VIEW_ERP_TRANSACTION_METADATA` for ERP reads      | `AUTH-0031` / invalid scope               |
| Missing `MANAGE_ERP_TRANSACTION_METADATA` for ERP writes   | `AUTH-0031` / invalid scope               |
| ERP integrations are not enabled for the account           | `ERP-0018` / forbidden                    |
| No active QuickBooks Online connection                     | `ERP-0001` / no active connection         |
| QuickBooks Online connection requires attention            | `ERP-0016` / connection action required   |
| Transaction not found for this account                     | `ERP-0026` / transaction not found        |
| Transaction already synced                                 | `ERP-0027` / transaction already synced   |
| Transaction export is in progress                          | `ERP-0052` / transaction sync in progress |
| Reference item is the wrong type or not in this connection | `ERP-0019` / invalid reference type       |
| Reference item is inactive                                 | `ERP-0028` / stale reference item         |
| ERP memo exceeds 4000 characters                           | `ERP-0029` / memo too long                |

***

## Notes and limits

* `recordId` is the transaction record ID returned by transaction queries.
* Annotation updates are partial: omitted fields stay unchanged, and `null` clears.
* ERP metadata updates are also partial: omitted fields stay unchanged, and supported nullable fields can be cleared with `null`.
* In `updateTransactionMetadata`, `erpTransactionMetadata: null` is a no-op, not a clear.
* In `updateTransactionMetadata`, `erpTransactionMetadata: {}` is invalid. Omit the field instead.
* If both an ERP reference ID and a name are provided for the same field, the reference ID wins.
* `categoryName` creates or selects a QuickBooks Online account with account type `Expense`.
* `vendorName` creates or selects a QuickBooks Online vendor.
* `customerName` creates or selects a QuickBooks Online customer.
* GraphQL pagination uses `OffsetInput`, `limit` defaults to 20 and is capped by the API pagination cap.
