# How to work with enriched transaction data
URL: https://www.quiltt.dev/get-started/tutorials/transaction-enrichment
Description: Turn raw bank transactions into clean categories, merchant names, and logos with Quiltt Enrichment and the GraphQL API. React tutorial with code examples.
Navigation: get-started → tutorials → transaction-enrichment
Tags: Tutorials, Transactions, Enrichment
Content Length: 11k characters

Raw bank transactions are messy. A coffee purchase arrives as `SQ *BLUE BOTTLE 8005551234 CA` with no category and no merchant name. This guide shows you how to turn that into a clean category, a readable merchant name, and a logo using Quiltt Enrichment and the GraphQL API.

What you'll build:

1. Enable a Transaction Enrichment provider in the Dashboard
2. Query enriched fields from a Transaction's `remoteData`
3. Map the provider payload into a display model
4. Render a transaction list with categories, merchant names, and logos

The examples use [React](/connector/sdk/react) and `@quiltt/react`, but the GraphQL query and the mapping logic apply to any client.

**Time:** ~15 minutes
**Level:** Intermediate

## How transaction enrichment works

Quiltt normalizes core Transaction fields (`amount`, `date`, `description`, `entryType`) across every aggregator. Enrichment adds context on top of those fields: a spending category, the real merchant behind the descriptor, and a logo.

When you enable an Enrichment provider, Quiltt sends every synced Transaction to that provider automatically as part of the standard Connection sync:

1. A user connects an account through the [Connector](/connector).
2. Quiltt syncs the Connection and sends each new Transaction to your enabled provider.
3. Quiltt attaches the provider's response to the Transaction's `remoteData` field.
4. Quiltt fires a `connection.synced.successful` webhook once the enriched data is ready.

Enriched data lives under `remoteData` rather than on the normalized Transaction, so you get the provider's full payload without losing the normalized fields. Quiltt supports four providers:

| Provider | Transaction Enrichment | Profile Insights |
| -------- | ---------------------- | ---------------- |
| [FinGoal](/integrations/enrichment/fingoal) | Yes | Yes |
| [MX](/integrations/enrichment/mx) | Yes | No |
| [Ntropy](/integrations/enrichment/ntropy) | Yes | No |
| [Pave](/integrations/enrichment/pave) | Yes | Yes |

This guide uses [Ntropy](/integrations/enrichment/ntropy). Each provider exposes its own payload under its own key in `remoteData`, so the same pattern works for any of them. Swap the field selection to match the provider's shape.

## Prerequisites

- [Quiltt Dashboard](https://dashboard.quiltt.dev) account ([sign up](https://dashboard.quiltt.dev/signup))
- A React app that authenticates with a Session token and renders the Connector. If you haven't set this up, follow the [Authentication tutorial](/get-started/tutorials/authentication) first.
- At least one Connection with synced transactions to enrich. A Connection is created when a user links a financial institution through the Connector, so connect an account first if you don't have one yet (see the [Connectors tutorial](/get-started/tutorials/connectors)).

Warning:
Enrichment runs on every Profile in the Environment where you enable it. On non-sandbox Environments, providers may bill per enriched transaction. Enable enrichment in a `SANDBOX` Environment first, and review your order form before enabling it in `PRODUCTION`.

## 1. Enable an enrichment provider

Turn on Ntropy for your Environment. In the [Dashboard](https://dashboard.quiltt.dev):

1. Open the **Integrations** link within your Environment.
2. Select **Ntropy** under Enrichment.
3. Enable the integration. In `SANDBOX`, Quiltt provisions a shared key so you can test without your own Ntropy account.

Quiltt enriches new transactions as they sync. Existing transactions on healthy Connections are reprocessed during the next sync cycle. Expect historical data to finish within 24 hours after you enable the integration.

## 2. Query enriched transactions

Enriched data is available on each Transaction's `remoteData` field. Request the normalized fields you already use, then add the provider block for the enrichment you want:

```graphql
query EnrichedTransactions {
  transactions(first: 20, sort: DATE_DESC) {
    edges {
      node {
        id
        date
        description
        amount
        entryType
        remoteData {
          ntropy {
            enrichment {
              response {
                categories {general}
                entities {
                  counterparty [name
                    logo
                    website]
                }
              }
              timestamp
            }
          }
        }
      }
    }
  }
}
```

A single enriched Transaction node looks like this:

```json
{
  "id": "txn_11VgTOO9DR1vbAZxb6zBLdb",
  "date": "2024-06-09",
  "description": "SQ *BLUE BOTTLE 8005551234 CA",
  "amount": -5.75,
  "entryType": "DEBIT",
  "remoteData": {
    "ntropy": {
      "enrichment": {
        "response": {
          "categories": ["general": "coffee shop"],
          "entities": {
            "counterparty": ["name": "Blue Bottle Coffee",
              "logo": "https://logos.ntropy.com/blue-bottle-coffee.com",
              "website": "blue-bottle-coffee.com"]
          }
        },
        "timestamp": "2024-06-09T14:22:05Z"
      }
    }
  }
}
```

The normalized `description` stays raw. The clean category and merchant come from the Ntropy block. `remoteData.ntropy` is `null` on a Transaction until enrichment finishes, so your code must handle its absence.

## 3. Map the provider payload to a display model

Reading nested provider fields in your components couples your UI to one provider's shape. Map the payload into a small display model instead, so a component renders the same fields no matter which provider produced them:

```typescript
// enrichment.ts
type EnrichedTransaction = [id: string
  date: string
  amount: number
  description: string
  category: string | null
  merchantName: string | null
  merchantLogo: string | null]

// Narrow the Ntropy block to the fields this UI needs.
type TransactionNode = {
  id: string
  date: string
  amount: number
  description: string
  remoteData?: {
    ntropy?: {
      enrichment?: {
        response?: {
          categories?: [conditional content] | null
          entities?: { counterparty?: [conditional content] | null } | null
        } | null
      } | null
    } | null
  } | null
}

  const response = node.remoteData?.ntropy?.enrichment?.response
  const counterparty = response?.entities?.counterparty

  return [conditional content]
}
```

Every field the provider hasn't filled in resolves to `null`, so the UI can fall back to the raw `description` for transactions that aren't enriched yet.

## 4. Display enriched transactions

Fetch the transactions with `useQuery` from `@quiltt/react`, map each node, and render the enriched fields. `QuilttProvider` supplies the Session token, so this component runs inside the provider you set up in the [Authentication tutorial](/get-started/tutorials/authentication):

```tsx
// TransactionList.tsx

const ENRICHED_TRANSACTIONS = gql`
  query EnrichedTransactions {
    transactions(first: 20, sort: DATE_DESC) {
      edges {
        node {
          id
          date
          description
          amount
          entryType
          remoteData {
            ntropy {
              enrichment {
                response {
                  categories {general}
                  entities {
                    counterparty [name
                      logo]
                  }
                }
              }
            }
          }
        }
      }
    }
  }
`

  const [data, loading, error] = useQuery(ENRICHED_TRANSACTIONS)

  if (loading) return Loading transactions...

  if (error) return Failed to load transactions.

  const transactions = data.transactions.edges.map((edge) =>
    toEnrichedTransaction(edge.node)
  )

  return (
    
{transactions.map((transaction) => (
        • [transaction.merchantLogo && (
            [true: transaction.merchantLogo]
          )]
          [transaction.merchantName ?? transaction.description]
          {transaction.category && [transaction.category]}
          [transaction.amount.toFixed(2)]

      ))}

  )
}
```

When enrichment runs, each row shows a merchant logo, a readable merchant name, and a category. When it does not run, the row falls back to the raw `description` and omits the logo and category.

## 5. Keep enriched data fresh

Enrichment updates whenever a Connection syncs, so read it in the same place you react to other transaction changes: the `connection.synced.successful` webhook. When you receive the event, refetch the affected Profile's transactions to pick up newly enriched fields.

If you already store transactions server-side, extend that flow to persist the enriched fields alongside the normalized ones. See the [Syncing Transactions tutorial](/get-started/tutorials/transaction-sync) for the full webhook handler and the [Remote Data guide](/api/remote-data) for accessing `remoteData` from the REST API.

## Verification

Confirm the full flow end to end:

1. In a `SANDBOX` Environment with Ntropy enabled, connect an account in the [Dashboard](https://dashboard.quiltt.dev) Connector preview. Use the **Mock** provider for guaranteed data.
2. Wait for the Connection to finish syncing (a few seconds in Sandbox).
3. Load the page that renders `TransactionList`.
4. Confirm transactions display a category and, where the merchant is recognized, a name and logo.
5. Open a Transaction whose logo is missing and confirm the row falls back to the raw `description` without errors.

## Troubleshooting

| Problem | Cause | Fix |
| ------- | ----- | --- |
| `remoteData.ntropy` is `null` | Enrichment hasn't finished for that Transaction | Read the enriched fields optionally and fall back to the raw `description`. Historical data can take up to 24 hours. |
| Every transaction is unenriched | Ntropy isn't enabled for this Environment | Enable Ntropy under **Integrations** in the Dashboard, then wait for the next sync cycle. |
| `category` is set but `merchantName` is `null` | Ntropy couldn't match a merchant for that descriptor | Expected for ambiguous descriptors. Fall back to the raw `description`. |
| Query returns no `remoteData` fields | The field selection is missing the provider block | Add the `ntropy { enrichment [...] }` block to your query, matching Step 2. |

## Next steps

You now display enriched transactions in your app. The same `remoteData` pattern works for any enrichment provider and for provider-specific data on other records.

### Related tutorials

- [Syncing Transactions](/get-started/tutorials/transaction-sync) - Persist transactions and enrichment server-side with webhooks
- [Authentication Tutorial](/get-started/tutorials/authentication) - Session token setup and management
- [Multi-Aggregator Connectors Tutorial](/get-started/tutorials/connectors) - Build a full connection flow

### Reference documentation

- [Enrichment Integrations](/integrations/enrichment) - Providers, Profile Insights, and how enrichment runs
- [Remote Data guide](/api/remote-data) - Access provider-specific data via GraphQL and REST
- [Transactions API reference](/api/transactions) - Complete transactions documentation
- [Ntropy integration](/integrations/enrichment/ntropy) - Category hierarchies and configuration

### Get help

[Contact support](mailto:support@quiltt.io)