Loading...
Search the Quiltt documentation
Loading...
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:
remoteDataThe examples use React and @quiltt/react, but the GraphQL query and the mapping logic apply to any client.
Time: ~15 minutes Level: Intermediate
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:
remoteData field.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 | Yes | Yes |
| MX | Yes | No |
| Ntropy | Yes | No |
| Pave | Yes | Yes |
This guide uses 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.
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.
Turn on Ntropy for your Environment. In the Dashboard:
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.
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:
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:
{
"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.
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:
// 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?: { general?: string | null } | null
entities?: { counterparty?: { name?: string | null; logo?: string | null } | null } | null
} | null
} | null
} | null
} | null
}
export function toEnrichedTransaction(node: TransactionNode): EnrichedTransaction {
const response = node.remoteData?.ntropy?.enrichment?.response
const counterparty = response?.entities?.counterparty
return {
id: node.id,
date: node.date,
amount: node.amount,
description: node.description,
// Fall back to the raw description when enrichment hasn't run yet.
category: response?.categories?.general ?? null,
merchantName: counterparty?.name ?? null,
merchantLogo: counterparty?.logo ?? null,
}
}
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.
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:
// TransactionList.tsx
import { gql, useQuery } from '@quiltt/react'
import { toEnrichedTransaction } from './enrichment'
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
}
}
}
}
}
}
}
}
}
}
`
export function TransactionList() {
const { data, loading, error } = useQuery(ENRICHED_TRANSACTIONS)
if (loading) return <p>Loading transactions...</p>
if (error) return <p>Failed to load transactions.</p>
const transactions = data.transactions.edges.map((edge) =>
toEnrichedTransaction(edge.node)
)
return (
<ul>
{transactions.map((transaction) => (
<li key={transaction.id}>
{transaction.merchantLogo && (
<img src={transaction.merchantLogo} alt="" width={24} height={24} />
)}
<span>{transaction.merchantName ?? transaction.description}</span>
{transaction.category && <span>{transaction.category}</span>}
<span>{transaction.amount.toFixed(2)}</span>
</li>
))}
</ul>
)
}
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.
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 for the full webhook handler and the Remote Data guide for accessing remoteData from the REST API.
Confirm the full flow end to end:
SANDBOX Environment with Ntropy enabled, connect an account in the Dashboard Connector preview. Use the Mock provider for guaranteed data.TransactionList.description without errors.| 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. |
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.