JSON over HTTPS. Send Content-Type: application/json.
Auth
API key in the Authorization header.
Current engine
3.2.0+af7d1cc4
Versioning. Everything under /v1 is additive-only: fields are never removed,
renamed or re-typed, so an integration written today keeps working while the matching engine improves behind it.
Treat unknown response fields as ignorable. Breaking changes ship as /v2, side by side. Every response
carries X-Tanzim-Engine-Version so you can tell which build answered.
Quickstart
1. Get a test key (instant, free): create one here, or
curl -X POST https://financial-core-production.up.railway.app/v1/keys -H "Content-Type: application/json" -d '{}'. 2. Send a transaction — if you created a key in this browser it is already filled into the snippets below:
import java.net.URI;
import java.net.http.*;
public class Main {
public static void main(String[] args) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create("https://financial-core-production.up.railway.app/v1/enrich"))
.header("Authorization", "Bearer YOUR_API_KEY")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"description\": \"NETFLIX.COM 4029357689\"}"))
.build();
HttpResponse<String> res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "YOUR_API_KEY");
var content = new StringContent("{\"description\": \"NETFLIX.COM 4029357689\"}", Encoding.UTF8, "application/json");
var res = await client.PostAsync("https://financial-core-production.up.railway.app/v1/enrich", content);
Console.WriteLine(await res.Content.ReadAsStringAsync());
Always check matched before using merchant_name. When it is false (for example a
person-to-person transfer) the fields are null — see the live examples at
the playground.
Analyse a bank statement
Upload a statement and get every transaction parsed, classified and enriched, plus spending insights. PDF (with selectable text),
CSV and Excel (.xlsx) are supported, up to 8 MB. The file is processed in memory and never stored.
import requests
with open("statement.pdf", "rb") as f:
r = requests.post("https://financial-core-production.up.railway.app/v1/statements/analyze",
headers={"Authorization": "Bearer YOUR_API_KEY"}, files={"file": f}, timeout=60)
r.raise_for_status()
result = r.json()
print(result["statement"]["confidence"], result["insights"]["summary"]["spending"])
The response has three parts: statement (what was read, with a confidence grade and a reconciliation self-check
against the statement's own balances), transactions (each with direction, kind and a full
enrichment), and insights (summary, categories, top merchants, monthly trend, recurring payments, transfers,
and how much spending could be identified). Show statement.warnings to your user. If you already have the transactions as data,
send them to POST /v1/transactions/analyze instead. Statements are limited to 20 per hour per key.
Privacy. Statement contents are not stored. Only merchant narrations Tanzim could not identify are kept to improve
matching, with long digit runs and e-mail addresses masked and person-to-person transfers excluded.
Authentication
Send your key as Authorization: Bearer tz_test_…, or as X-API-Key: tz_test_… if your tooling
can't set that header. Verify a key with GET /v1/me. Rotate by creating a new key, switching over, then
DELETE /v1/keys/current with the old one. Keep keys out of public front-end code.
Live example inputs and outputs · No authentication
Sample transactions run through the engine right now, so the output is always what the current build really returns. The docs pages are generated from this.
The raw transaction string exactly as it appears on the statement or card feed. e.g. "NETFLIX.COM 4029357689"
mcc
string | null
Optional 4-digit Merchant Category Code from the card network. Used only as a fallback category when the merchant cannot be identified. mcc_code is accepted as an alias. e.g. "5968"
Up to 100 transactions per call. Each item counts once toward your rate limit. Results are in input order; one unmatched item never fails the whole batch.
Analyse a bank or card statement · API key required
Upload a statement (PDF, CSV or Excel) and get back every transaction parsed, dated, classified and enriched with merchant details, plus insights: spending by category and merchant, monthly trend, recurring payments and subscriptions, transfers, fees and cash. The file is processed in memory and never stored. statement.confidence and statement.reconciliation tell you whether the parse can be trusted: if the statement has a balance column, every row is checked against it.
Too many requests. Wait Retry-After seconds, then retry. ErrorResponse
413
The request body is too large (1 MB for most endpoints; statements may be larger, see /v1/meta). ErrorResponse
415
That file type isn't supported. Upload a PDF, CSV or Excel (.xlsx) statement. ErrorResponse
422
The file was read but no transactions could be extracted (for example a scanned PDF, a password-protected PDF, or a file that isn't a statement). details[].reason says which. ErrorResponse
POST/v1/transactions/analyze
Analyse a list of transactions · API key required
Same enrichment and insights as /statements/analyze, for transactions you already have as structured data (up to 5000). Include balance when you can: it makes direction detection and the self-check reliable.
Too many requests. Wait Retry-After seconds, then retry. ErrorResponse
413
The request body is too large (1 MB for most endpoints; statements may be larger, see /v1/meta). ErrorResponse
422
The file was read but no transactions could be extracted (for example a scanned PDF, a password-protected PDF, or a file that isn't a statement). details[].reason says which. ErrorResponse
Objects
Generated from the live OpenAPI document, so it always matches what the API really returns.
The statement file: PDF (with selectable text), CSV or Excel (.xlsx).
CategoryCount
Field
Type
Description
namerequired
string
merchantsrequired
integer
CategoryInsight
Field
Type
Description
categoryrequired
string
Merchant category; unidentified spending is grouped as Unidentified.
amountrequired
number
countrequired
integer
sharerequired
number
Share of total spending, 0 to 1.
top_merchantsrequired
array of string
CounterpartyInfo
Field
Type
Description
namerequired
string
The other person's name with sensible capitalisation ("RAO EHTISHAM ALI KHAN" -> "Rao Ehtisham Ali Khan"). e.g. "Ali Raza"
raw_namerequired
string
Exactly as printed on the statement (the account title returned by the bank's title fetch). e.g. "ALI RAZA"
bank
string | null
The other party's bank or wallet, when the narration shows it. e.g. "HBL"
channel
string | null
ibft, raast, wallet, transfer, or card (a card payment made to a person's name).
direction
string | null
in or out, from the wording of the narration. A statement's debit/credit is more reliable when you have it.
confidencerequired
string
high (two or more words) or low (a single word).
CounterpartyInsight
Field
Type
Description
namerequired
string
sentrequired
number
receivedrequired
number
countrequired
integer
EnrichRequest
Field
Type
Description
descriptionrequired
string
The raw transaction string exactly as it appears on the statement or card feed. e.g. "NETFLIX.COM 4029357689"
mcc
string | null
Optional 4-digit Merchant Category Code from the card network. Used only as a fallback category when the merchant cannot be identified. mcc_code is accepted as an alias. e.g. "5968"
EnrichResponse
Field
Type
Description
inputrequired
string
The description you sent, echoed back (handy for batches).
matchedrequired
boolean
True when a specific merchant was identified. Check this before using merchant_name.
merchant_name
string | null
Canonical merchant name. null when matched is false. e.g. "Netflix"
category
string | null
Spending category. When matched is false this is only set if you sent an mcc. e.g. "Subscriptions & OTT"
subcategory
string | null
e.g. "OTT - International"
country
string | null
ISO 3166-1 alpha-2 country the merchant operates in, where known; otherwise null. e.g. "PK"
logo_url
string | null
Logo/icon image URL. Checked to load when last verified; null when no working logo is known, so show your own placeholder (e.g. the merchant's initial).
website
string | null
e.g. "https://netflix.com"
domain
string | null
e.g. "netflix.com"
mcc
string | null
The merchant's usual Merchant Category Code, where known. e.g. "5968"
confidencerequired
number
How sure Tanzim is that merchant_name is right, 0 to 1. It is derived from the match method and similarity score, not a calibrated probability: treat >= 0.9 as reliable and anything lower as worth a human glance. Always 0 when matched is false. e.g. 0.99
For person-to-person transfers whose narration names the other person (IBFT / Raast / wallet title fetch): who it is. When present, matched is false unless a real merchant was identified.
request_idrequired
string
Unique id for this request; also sent as the X-Request-Id header.
engine_versionrequired
string
Version of the matching engine + merchant data that produced this result. e.g. "3.1.0+18b095da"
EnrichResult
Field
Type
Description
inputrequired
string
The description you sent, echoed back (handy for batches).
matchedrequired
boolean
True when a specific merchant was identified. Check this before using merchant_name.
merchant_name
string | null
Canonical merchant name. null when matched is false. e.g. "Netflix"
category
string | null
Spending category. When matched is false this is only set if you sent an mcc. e.g. "Subscriptions & OTT"
subcategory
string | null
e.g. "OTT - International"
country
string | null
ISO 3166-1 alpha-2 country the merchant operates in, where known; otherwise null. e.g. "PK"
logo_url
string | null
Logo/icon image URL. Checked to load when last verified; null when no working logo is known, so show your own placeholder (e.g. the merchant's initial).
website
string | null
e.g. "https://netflix.com"
domain
string | null
e.g. "netflix.com"
mcc
string | null
The merchant's usual Merchant Category Code, where known. e.g. "5968"
confidencerequired
number
How sure Tanzim is that merchant_name is right, 0 to 1. It is derived from the match method and similarity score, not a calibrated probability: treat >= 0.9 as reliable and anything lower as worth a human glance. Always 0 when matched is false. e.g. 0.99
For person-to-person transfers whose narration names the other person (IBFT / Raast / wallet title fetch): who it is. When present, matched is false unless a real merchant was identified.
What you're building (shown only to you). e.g. "My budgeting app"
email
string | null
Optional, but lets us tell you about breaking changes and quota.
source
string | null
Free-form label for where the key was requested from.
KindTotal
Field
Type
Description
kindrequired
string
countrequired
integer
debit_amountrequired
number
credit_amountrequired
number
LargeTransaction
Field
Type
Description
daterequired
string
descriptionrequired
string
amountrequired
number
merchant
string | null
kindrequired
string
Match
Field
Type
Description
methodrequired
string
How the merchant was identified: exact (the whole string is a known name), substring (a known name appears inside it), fuzzy (a close misspelling/partial match), mcc_fallback (merchant unknown, category taken from the MCC you sent) or none. e.g. "substring"
matched_on
string | null
The known name/alias that matched, normalised. Useful for debugging. e.g. "NETFLIX COM"
Self-check against the statement's balances. null when the statement has no balance column.
StatementTransaction
Field
Type
Description
indexrequired
integer
Position in the statement, starting at 0.
date
string | null
descriptionrequired
string
amountrequired
number
Always positive; see direction.
directionrequired
string
debit (money out), credit (money in) or unknown.
balance
number | null
kindrequired
string
What sort of transaction this is: merchant, transfer_in, transfer_out (person-to-person), cash, fee, income, interest, refund, bill_payment or unknown.
counterparty
string | null
The other person in a transfer, when the narration names them (capitalisation normalised).
payee_id
string | null
Stable id of that person within this statement: different spellings of one name share it.
counterparty_bank
string | null
The other person's bank or wallet, when shown.
channel
string | null
ibft, raast, wallet, transfer or card.
identifiedrequired
boolean
True when the merchant was identified with confidence of at least 0.9. Weaker guesses stay in enrichment (show them as "possibly ...") but are not used for categories, merchants or kinds.
Transfers where the statement names the other person.
named_sharerequired
number
Many statements print only a reference, not a name: when this is low, regular_unnamed_transfers is the useful view.
UnidentifiedItem
Field
Type
Description
descriptionrequired
string
countrequired
integer
amountrequired
number
Usage
Field
Type
Description
last_24hrequired
integer
Transactions enriched with this key in the last 24 hours.
Errors
Every error has the same shape and an X-Request-Id header:
{
"error": {
"code": "invalid_api_key",
"message": "The API key is unknown or has been revoked.",
"request_id": "req_…"
}
}
code
HTTP
Meaning
invalid_request
400
The request body or a parameter is missing or malformed. details lists the offending fields.
missing_api_key
401
No API key was sent. Use Authorization: Bearer <key> or X-API-Key: <key>.
invalid_api_key
401
The API key is unknown or has been revoked.
not_found
404
No such endpoint.
method_not_allowed
405
That endpoint does not support this HTTP method.
payload_too_large
413
The request body is too large (1 MB for most endpoints; statements may be larger, see /v1/meta).
rate_limited
429
Too many requests. Wait Retry-After seconds, then retry.
unsupported_file
415
That file type isn't supported. Upload a PDF, CSV or Excel (.xlsx) statement.
statement_unreadable
422
The file was read but no transactions could be extracted (for example a scanned PDF, a password-protected PDF, or a file that isn't a statement). details[].reason says which.
internal_error
500
Something went wrong on our side. Quote the request_id if you contact us.
Branch on error.code, not on the message text.
Rate limits
Test keys: 120 transactions per minute and 10000 per day (a batch of N counts as N).
Responses include X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds);
a 429 includes Retry-After. Batches take up to 100 items.
Integration tips
Any language works. It is plain REST + JSON. For a typed client, generate one from /v1/openapi.json
(e.g. openapi-generator, openapi-typescript, oapi-codegen) or import that URL into Postman/Insomnia.
Minimal dependency-free clients for Python and JavaScript are in backend/sdk/.
Retries are safe. Enrichment is read-only and idempotent: retry 429 (after Retry-After) and 5xx with backoff.
Batch when you can. One request for 100 transactions beats 100 requests.
Use confidence. ≥ 0.9 is reliable; lower deserves a human glance or a "suggested" label in your UI.
Send the whole string exactly as the bank gave it, noise and all. Don't pre-clean it; the engine does that.
Send mcc when you have it. It gives a category even for merchants we don't know yet.
Ignore unknown fields in responses so additive improvements never break you.
Test-key requests are logged with their input so we can improve matching; live keys never store your inputs. Don't send real personal data from test keys. Failed and low-confidence enrichments are reviewed to add
missing merchants, which is how coverage improves over time.