Categorize expenses
Every bookkeeper knows the drudgery: a CSV of five hundred card transactions, each needing a category before the books close. This endpoint reads the merchant string, amount and any memo you supply, then assigns each line to the accounting bucket it belongs in — travel, office supplies, software, meals — so your ledger closes faster and your accountant stops guessing.
Run it online
Run this on our servers with your account. Free tools run in your browser; this one bills your KIT balance per the price above.
Who actually needs this
Bookkeepers reconciling client accounts, finance teams closing month-end, and expense-management tools that ingest raw bank feeds all hit the same wall: transaction descriptions are cryptic. A merchant string like 'SQ *DARK ROAST LLC' tells a human almost nothing without context, and manually tagging thousands of rows every month is not a job anyone wants. Expense categorization exists to remove that specific chore, not to replace the bookkeeper's judgment on edge cases.
What you send and what comes back
You POST an array of transactions — description, amount, currency, and optionally your own category list — to /text/expense-categorize. The task runs asynchronously: you get a task_id immediately and the categorized result later, either pushed to your signed webhook or waiting behind a signed link valid for 24 hours. Each transaction comes back with its assigned category and, where useful, a short note on why that bucket was chosen, so a human reviewer can spot-check without re-deriving the logic.
How the categorization actually works
The engine reads the merchant name, transaction amount, and memo text together, matching patterns against your supplied taxonomy or a sensible general-ledger default if you don't provide one. It's the same instinct an experienced bookkeeper applies — 'this recurring $12.99 charge from a software vendor is a subscription, not a one-off purchase' — turned into something you can call from code instead of scrolling a spreadsheet.
Fitting it into a real pipeline
Because it's async and metered per item, you can batch a whole month of transactions in one call and let the webhook notify your accounting system when it's done — no polling loop needed. Bulk batches are the normal use case, not an exception: send 50 rows or 50,000, and pricing scales predictably with volume rather than punishing you for the size of your ledger.
Where it stops
This is pattern-based categorization, not tax advice or audit certification — ambiguous transactions (a $400 charge from a general retailer, say) will get a best-guess category with lower confidence signals baked into the note field, and your finance team should keep a review step for anything above your materiality threshold. That honesty is the point: a categorizer that silently guesses wrong is worse than one that flags uncertainty.
What you can do with it
Month-end close automation
Pull the bank feed, categorize every transaction in one async batch, and hand the accountant a pre-sorted ledger instead of a raw CSV.
Expense app backends
Auto-tag employee card swipes the moment they land, so reimbursement approval queues show a category instead of a blank field.
Multi-entity bookkeeping firms
Run each client's transactions against their own chart of accounts by passing a custom taxonomy per request.
Spend-analysis dashboards
Feed categorized transactions into a BI tool to chart spend by department or vendor type without manual tagging first.
FAQ
How does the expense categorization API decide a category?
It reads the merchant description, amount and memo together and matches them against your chart of accounts or a default general-ledger taxonomy, returning the best-fit category per transaction.
Can I use my own category list instead of a default one?
Yes, pass your taxonomy in the request and every transaction is matched against it instead of the built-in default.
Is there a free tier?
There's no free tier — free tiers get abused and slow everyone down. Access runs on a prepaid ForHosting KIT balance: top up from $10.00 (it never expires) and each request is charged at its published price, so a call with no balance returns HTTP 402. No subscription, no tokens, no invented credits, and a failed task is never charged.
How much does it cost?
$0.003 per request plus $0.0135 per item categorized, so a 200-transaction batch costs a predictable, fixed amount you can calculate before you send it.
How do I get the results?
The task runs asynchronously and returns a task_id immediately; results arrive at your signed webhook or via a signed link valid for 24 hours, whichever you configure.
What happens if a transaction can't be categorized confidently?
It's still returned with a best-guess category and a note flagging lower confidence, so your review workflow can catch it instead of silently misfiling it.
Is this a replacement for an accountant?
No — it automates the repetitive sorting step so your bookkeeper or accountant can focus on judgment calls and exceptions instead of manual tagging.
Can I send bulk batches of thousands of transactions?
Yes, bulk is the expected use case; you send one array of transactions and pricing scales linearly per item with no arbitrary batch cap.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, and soon from our app, email and Telegram.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/text/expense-categorize \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"items":["valor-1","valor-2"]}'const res = await fetch("https://api.kit.forhosting.com/text/expense-categorize", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"items": [
"valor-1",
"valor-2"
]
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/text/expense-categorize",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"items": [
"valor-1",
"valor-2"
]
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/text/expense-categorize", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"items":["valor-1","valor-2"]}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"items":["valor-1","valor-2"]}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/text/expense-categorize", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"items": [
"valor-1",
"valor-2"
]
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "text.expense_categorize",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_tokens | 20000 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
422 | task_failed | The task failed after 3 retries. You are never charged for it. |