Transaction Categorization
Enrich financial transactions with brand data — logos, merchant names, and industry classification — to build better banking and expense management experiences.
The Problem
Bank transaction descriptors are cryptic. STRIPE* VERCEL INC 800-555-0199 CA means nothing to most users. Fintech apps need to decode these into recognizable brands.
The Solution
Use Orsa's Transaction Identifier to match descriptors to brands, then enrich with logos, colors, and NAICS codes for categorization.
Pipeline
Raw Transaction → Orsa Transaction ID → Brand Data → Enriched Transaction
↓ ↓ ↓ ↓
"STRIPE* VERCEL" domain: vercel.com logo: ✓ "Vercel - Software"
confidence: 0.92 color: #000 category: "Technology"Implementation
Single Transaction
import { Orsa } from 'orsa';
const orsa = new Orsa({ apiKey: process.env.ORSA_API_KEY });
async function enrichTransaction(descriptor: string, country?: string) {
const result = await orsa.brand.transactionIdentifier({
transactionInfo: descriptor,
countryGl: country,
});
return {
merchant: result.name,
domain: result.domain,
logo: result.logos[0]?.url,
color: result.colors[0],
category: result.industry,
confidence: result.transaction.confidence,
};
}
// Usage
const enriched = await enrichTransaction('STRIPE* VERCEL INC', 'US');
// { merchant: "Vercel", domain: "vercel.com", logo: "...", category: "Cloud Computing", confidence: 0.92 }Python
from orsa import Orsa
orsa = Orsa(api_key=os.environ["ORSA_API_KEY"])
def enrich_transaction(descriptor: str, country: str = None) -> dict:
result = orsa.brand.transaction_identifier(
transaction_info=descriptor,
country_gl=country,
)
return {
"merchant": result.name,
"domain": result.domain,
"logo": result.logos[0].url if result.logos else None,
"color": result.colors[0] if result.colors else None,
"category": result.industry,
"confidence": result.transaction.confidence,
}Batch Processing
async function enrichBatch(transactions: Array<{ id: string; descriptor: string }>) {
const results = await Promise.allSettled(
transactions.map(async (tx) => ({
id: tx.id,
...(await enrichTransaction(tx.descriptor)),
}))
);
return results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
}
// Process in chunks to respect rate limits
async function processAll(transactions: Array<{ id: string; descriptor: string }>) {
const chunkSize = 20;
const results = [];
for (let i = 0; i < transactions.length; i += chunkSize) {
const chunk = transactions.slice(i, i + chunkSize);
const enriched = await enrichBatch(chunk);
results.push(...enriched);
// Respect rate limits
if (i + chunkSize < transactions.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return results;
}Adding NAICS Classification
For expense categorization, combine transaction identification with NAICS codes:
async function categorizeTransaction(descriptor: string) {
const brand = await orsa.brand.transactionIdentifier({
transactionInfo: descriptor,
});
// Get NAICS codes for expense categorization
const naics = await orsa.brand.naics({ domain: brand.domain });
return {
merchant: brand.name,
logo: brand.logos[0]?.url,
naicsCode: naics.naics_codes[0]?.code,
naicsTitle: naics.naics_codes[0]?.title,
// Map NAICS to your expense categories
expenseCategory: mapNaicsToCategory(naics.naics_codes[0]?.code),
};
}
function mapNaicsToCategory(naicsCode: string): string {
const prefix = naicsCode?.slice(0, 2);
const mapping: Record<string, string> = {
'44': 'Shopping',
'45': 'Shopping',
'51': 'Software & Technology',
'52': 'Financial Services',
'72': 'Food & Dining',
'48': 'Transportation',
'71': 'Entertainment',
};
return mapping[prefix] ?? 'Other';
}Credit Cost
| Operation | Endpoint | Credits |
|---|---|---|
| Identify transaction | GET /v1/brand/transaction-identifier | 10 |
| NAICS classification (optional) | GET /v1/brand/naics | 5 |
| Per transaction | 10-15 credits |
Tips
- Use
maxSpeed=truefor real-time UIs where you need sub-second responses — it only checks cached data. - Cache results by normalized descriptor in your database. The same merchant string will appear repeatedly.
- Pass
country_glwhen available — it significantly improves match accuracy for international transactions. - Handle low-confidence matches (< 0.7) by showing the raw descriptor alongside the brand match.
- Batch process historical transactions during off-peak hours to avoid rate limits.