API documentation

One request turns a raw transaction string into a merchant, category and more.

Overview

Base URLhttps://financial-core-production.up.railway.app/v1
FormatJSON over HTTPS. Send Content-Type: application/json.
AuthAPI key in the Authorization header.
Current engine3.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:

curl -X POST https://financial-core-production.up.railway.app/v1/enrich \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"description": "NETFLIX.COM 4029357689"}'
import requests

resp = requests.post(
    "https://financial-core-production.up.railway.app/v1/enrich",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={"description": "NETFLIX.COM 4029357689"},
    timeout=10,
)
resp.raise_for_status()
print(resp.json()["merchant_name"])
const res = await fetch("https://financial-core-production.up.railway.app/v1/enrich", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ description: "NETFLIX.COM 4029357689" }),
});
const data = await res.json();
console.log(data.merchant_name);
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
)

func main() {
	body := bytes.NewBufferString(`{"description": "NETFLIX.COM 4029357689"}`)
	req, _ := http.NewRequest("POST", "https://financial-core-production.up.railway.app/v1/enrich", body)
	req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	out, _ := io.ReadAll(resp.Body)
	fmt.Println(string(out))
}
require "net/http"
require "json"

uri = URI("https://financial-core-production.up.railway.app/v1/enrich")
req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer YOUR_API_KEY", "Content-Type" => "application/json")
req.body = { description: "NETFLIX.COM 4029357689" }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
puts JSON.parse(res.body)["merchant_name"]
<?php
$ch = curl_init("https://financial-core-production.up.railway.app/v1/enrich");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer YOUR_API_KEY", "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode(["description" => "NETFLIX.COM 4029357689"]),
]);
$data = json_decode(curl_exec($ch), true);
echo $data["merchant_name"];
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());

3. Read the result:

{
  "input": "NETFLIX.COM 4029357689",
  "matched": true,
  "merchant_name": "Netflix",
  "category": "Subscriptions & OTT",
  "subcategory": "OTT - International",
  "country": null,
  "logo_url": "https://www.google.com/s2/favicons?domain=netflix.com&sz=128",
  "website": "https://netflix.com",
  "domain": "netflix.com",
  "mcc": "5968",
  "confidence": 0.99,
  "match": {
    "method": "substring",
    "matched_on": "NETFLIX COM"
  },
  "counterparty": null,
  "request_id": "req_…",
  "engine_version": "3.2.0+af7d1cc4"
}

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.

curl -X POST https://financial-core-production.up.railway.app/v1/statements/analyze \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@statement.pdf"
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.

Endpoints

GET/v1/health

Liveness check · No authentication

Responses
StatusMeaning
200Successful Response Health

GET/v1/meta

Engine and data summary · No authentication

Responses
StatusMeaning
200Successful Response Meta

GET/v1/examples

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.

Responses
StatusMeaning
200Successful Response ExamplesResponse

POST/v1/keys

Create a test API key · No authentication

Self-serve and instant: no approval step. Returns the key once. Limited per IP address.

Request body · KeyRequest
FieldTypeDescription
namestring | nullWhat you're building (shown only to you). e.g. "My budgeting app"
emailstring | nullOptional, but lets us tell you about breaking changes and quota.
sourcestring | nullFree-form label for where the key was requested from.
Responses
StatusMeaning
201Successful Response KeyCreated
400The request body or a parameter is missing or malformed. details lists the offending fields. ErrorResponse
429Too many requests. Wait Retry-After seconds, then retry. ErrorResponse
Example request
{
  "name": "My budgeting app",
  "email": "dev@example.com"
}
Example response
{
  "key": "tz_test_Xk3…",
  "id": "key_0a1b2c3d4e5f6a7b",
  "prefix": "tz_test_Xk3d",
  "environment": "test",
  "created_at": "2026-01-01T12:00:00+00:00",
  "rate_limits": {
    "per_minute": 120,
    "per_day": 10000
  },
  "message": "Store this key now; it cannot be shown again."
}

GET/v1/me

Check your API key · API key required

The quickest way to verify an integration: returns 200 if your key works.

Responses
StatusMeaning
200Successful Response Me
401missing_api_key or invalid_api_key. ErrorResponse

DELETE/v1/keys/current

Revoke the key you are using · API key required

Use to rotate: create a new key, switch over, then revoke the old one. Irreversible.

Responses
StatusMeaning
200Successful Response Revoked
401missing_api_key or invalid_api_key. ErrorResponse

POST/v1/enrich

Enrich one transaction · API key required

Identify the merchant behind a raw transaction string and return structured details.

Request body · EnrichRequest
FieldTypeDescription
descriptionrequiredstringThe raw transaction string exactly as it appears on the statement or card feed. e.g. "NETFLIX.COM 4029357689"
mccstring | nullOptional 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"
Responses
StatusMeaning
200Successful Response EnrichResponse
400The request body or a parameter is missing or malformed. details lists the offending fields. ErrorResponse
401missing_api_key or invalid_api_key. ErrorResponse
429Too many requests. Wait Retry-After seconds, then retry. ErrorResponse
Example request
{
  "description": "NETFLIX.COM 4029357689"
}
Example response
{
  "input": "NETFLIX.COM 4029357689",
  "matched": true,
  "merchant_name": "Netflix",
  "category": "Subscriptions & OTT",
  "subcategory": "OTT - International",
  "country": null,
  "logo_url": "https://www.google.com/s2/favicons?domain=netflix.com&sz=128",
  "website": "https://netflix.com",
  "domain": "netflix.com",
  "mcc": "5968",
  "confidence": 0.99,
  "match": {
    "method": "substring",
    "matched_on": "NETFLIX COM"
  },
  "counterparty": null,
  "request_id": "req_9f2c41d07a3b5e6c8d10",
  "engine_version": "3.2.0+af7d1cc4"
}

POST/v1/enrich/batch

Enrich many transactions · API key required

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.

Request body · BatchRequest
FieldTypeDescription
itemsrequiredarray of EnrichRequest1 to 100 transactions. Results come back in the same order.
Responses
StatusMeaning
200Successful Response BatchResponse
400The request body or a parameter is missing or malformed. details lists the offending fields. ErrorResponse
401missing_api_key or invalid_api_key. ErrorResponse
429Too many requests. Wait Retry-After seconds, then retry. ErrorResponse
Example request
{
  "items": [
    {
      "description": "NETFLIX.COM 4029357689"
    },
    {
      "description": "From Ali Raza (SadaPay) (Incoming transfer)"
    }
  ]
}
Example response
{
  "request_id": "req_9f2c41d07a3b5e6c8d10",
  "engine_version": "3.2.0+af7d1cc4",
  "results": [
    {
      "input": "NETFLIX.COM 4029357689",
      "matched": true,
      "merchant_name": "Netflix",
      "category": "Subscriptions & OTT",
      "subcategory": "OTT - International",
      "country": null,
      "logo_url": "https://www.google.com/s2/favicons?domain=netflix.com&sz=128",
      "website": "https://netflix.com",
      "domain": "netflix.com",
      "mcc": "5968",
      "confidence": 0.99,
      "match": {
        "method": "substring",
        "matched_on": "NETFLIX COM"
      },
      "counterparty": null
    },
    {
      "input": "From Ali Raza (SadaPay) (Incoming transfer)",
      "matched": false,
      "merchant_name": null,
      "category": null,
      "subcategory": null,
      "country": null,
      "logo_url": null,
      "website": null,
      "domain": null,
      "mcc": null,
      "confidence": 0.0,
      "match": {
        "method": "none",
        "matched_on": null
      },
      "counterparty": {
        "name": "Ali Raza",
        "raw_name": "Ali Raza",
        "bank": "SadaPay",
        "channel": "wallet",
        "direction": "in",
        "confidence": "high"
      }
    }
  ]
}

POST/v1/statements/analyze

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.

Responses
StatusMeaning
200Successful Response AnalysisResponse
400The request body or a parameter is missing or malformed. details lists the offending fields. ErrorResponse
401missing_api_key or invalid_api_key. ErrorResponse
429Too many requests. Wait Retry-After seconds, then retry. ErrorResponse
413The request body is too large (1 MB for most endpoints; statements may be larger, see /v1/meta). ErrorResponse
415That file type isn't supported. Upload a PDF, CSV or Excel (.xlsx) statement. ErrorResponse
422The 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.

Request body · TransactionsRequest
FieldTypeDescription
transactionsrequiredarray of TransactionIn
currencystring | nullISO currency code, used for labelling only.
Responses
StatusMeaning
200Successful Response AnalysisResponse
400The request body or a parameter is missing or malformed. details lists the offending fields. ErrorResponse
401missing_api_key or invalid_api_key. ErrorResponse
429Too many requests. Wait Retry-After seconds, then retry. ErrorResponse
413The request body is too large (1 MB for most endpoints; statements may be larger, see /v1/meta). ErrorResponse
422The 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.

AnalysisResponse

FieldTypeDescription
request_idrequiredstring
engine_versionrequiredstring
statementrequiredStatementInfo
transactionsrequiredarray of StatementTransaction
insightsrequiredInsights

BatchRequest

FieldTypeDescription
itemsrequiredarray of EnrichRequest1 to 100 transactions. Results come back in the same order.

BatchResponse

FieldTypeDescription
request_idrequiredstring
engine_versionrequiredstring
resultsrequiredarray of EnrichResult

Body_analyze_statement_file_statements_analyze_post

FieldTypeDescription
filerequiredstringThe statement file: PDF (with selectable text), CSV or Excel (.xlsx).

CategoryCount

FieldTypeDescription
namerequiredstring
merchantsrequiredinteger

CategoryInsight

FieldTypeDescription
categoryrequiredstringMerchant category; unidentified spending is grouped as Unidentified.
amountrequirednumber
countrequiredinteger
sharerequirednumberShare of total spending, 0 to 1.
top_merchantsrequiredarray of string

CounterpartyInfo

FieldTypeDescription
namerequiredstringThe other person's name with sensible capitalisation ("RAO EHTISHAM ALI KHAN" -> "Rao Ehtisham Ali Khan"). e.g. "Ali Raza"
raw_namerequiredstringExactly as printed on the statement (the account title returned by the bank's title fetch). e.g. "ALI RAZA"
bankstring | nullThe other party's bank or wallet, when the narration shows it. e.g. "HBL"
channelstring | nullibft, raast, wallet, transfer, or card (a card payment made to a person's name).
directionstring | nullin or out, from the wording of the narration. A statement's debit/credit is more reliable when you have it.
confidencerequiredstringhigh (two or more words) or low (a single word).

CounterpartyInsight

FieldTypeDescription
namerequiredstring
sentrequirednumber
receivedrequirednumber
countrequiredinteger

EnrichRequest

FieldTypeDescription
descriptionrequiredstringThe raw transaction string exactly as it appears on the statement or card feed. e.g. "NETFLIX.COM 4029357689"
mccstring | nullOptional 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

FieldTypeDescription
inputrequiredstringThe description you sent, echoed back (handy for batches).
matchedrequiredbooleanTrue when a specific merchant was identified. Check this before using merchant_name.
merchant_namestring | nullCanonical merchant name. null when matched is false. e.g. "Netflix"
categorystring | nullSpending category. When matched is false this is only set if you sent an mcc. e.g. "Subscriptions & OTT"
subcategorystring | null e.g. "OTT - International"
countrystring | nullISO 3166-1 alpha-2 country the merchant operates in, where known; otherwise null. e.g. "PK"
logo_urlstring | nullLogo/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).
websitestring | null e.g. "https://netflix.com"
domainstring | null e.g. "netflix.com"
mccstring | nullThe merchant's usual Merchant Category Code, where known. e.g. "5968"
confidencerequirednumberHow 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
matchrequiredMatch
counterpartyCounterpartyInfo | nullFor 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_idrequiredstringUnique id for this request; also sent as the X-Request-Id header.
engine_versionrequiredstringVersion of the matching engine + merchant data that produced this result. e.g. "3.1.0+18b095da"

EnrichResult

FieldTypeDescription
inputrequiredstringThe description you sent, echoed back (handy for batches).
matchedrequiredbooleanTrue when a specific merchant was identified. Check this before using merchant_name.
merchant_namestring | nullCanonical merchant name. null when matched is false. e.g. "Netflix"
categorystring | nullSpending category. When matched is false this is only set if you sent an mcc. e.g. "Subscriptions & OTT"
subcategorystring | null e.g. "OTT - International"
countrystring | nullISO 3166-1 alpha-2 country the merchant operates in, where known; otherwise null. e.g. "PK"
logo_urlstring | nullLogo/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).
websitestring | null e.g. "https://netflix.com"
domainstring | null e.g. "netflix.com"
mccstring | nullThe merchant's usual Merchant Category Code, where known. e.g. "5968"
confidencerequirednumberHow 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
matchrequiredMatch
counterpartyCounterpartyInfo | nullFor 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.

ErrorBody

FieldTypeDescription
coderequiredstring e.g. "invalid_api_key"
messagerequiredstring
request_idrequiredstring
detailsarray of map of string | nullPer-field problems for invalid_request.

ErrorResponse

FieldTypeDescription
errorrequiredErrorBody

ExampleItem

FieldTypeDescription
descriptionrequiredstring
mccstring | null
noterequiredstring
resultrequiredEnrichResult

ExamplesResponse

FieldTypeDescription
engine_versionrequiredstring
examplesrequiredarray of ExampleItem

Identification

FieldTypeDescription
spending_transactionsrequiredinteger
identified_transactionsrequiredinteger
identified_share_of_transactionsrequirednumber
identified_share_of_spendingrequirednumberHow much of the spending was matched to a known merchant, 0 to 1.
unidentified_toprequiredarray of UnidentifiedItemThe biggest spending items Tanzim could not identify.

Insights

FieldTypeDescription
summaryrequiredSummary
by_kindrequiredarray of KindTotal
categoriesrequiredarray of CategoryInsight
top_merchantsrequiredarray of MerchantInsight
monthlyrequiredarray of MonthInsight
recurringrequiredarray of RecurringInsightRepeating payments (subscriptions, bills) detected from the dates and amounts.
monthly_recurring_totalrequirednumber
largest_transactionsrequiredarray of LargeTransaction
counterpartiesrequiredarray of CounterpartyInsight
payeesrequiredarray of PayeeInsightPeople you transact with, grouped across spellings, biggest first.
regular_unnamed_transfersrequiredarray of RegularTransferRepeating transfers with the same amount whose statement line names nobody: likely the same person or account. A pattern, not an identified payee.
transfer_summaryrequiredTransferSummary
identificationrequiredIdentification

KeyCreated

FieldTypeDescription
keyrequiredstringThe API key. Shown ONCE — store it now. e.g. "tz_test_..."
idrequiredstring
prefixrequiredstringFirst characters of the key, to recognise it later.
environmentrequiredstringtest for now. Test keys are rate limited and their inputs are logged to improve Tanzim.
created_atrequiredstring
rate_limitsrequiredRateLimits
messagerequiredstring

KeyRequest

FieldTypeDescription
namestring | nullWhat you're building (shown only to you). e.g. "My budgeting app"
emailstring | nullOptional, but lets us tell you about breaking changes and quota.
sourcestring | nullFree-form label for where the key was requested from.

KindTotal

FieldTypeDescription
kindrequiredstring
countrequiredinteger
debit_amountrequirednumber
credit_amountrequirednumber

LargeTransaction

FieldTypeDescription
daterequiredstring
descriptionrequiredstring
amountrequirednumber
merchantstring | null
kindrequiredstring

Match

FieldTypeDescription
methodrequiredstringHow 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_onstring | nullThe known name/alias that matched, normalised. Useful for debugging. e.g. "NETFLIX COM"

Me

FieldTypeDescription
key_idrequiredstring
prefixrequiredstring
environmentrequiredstring
namestring | null
created_atrequiredstring
rate_limitsrequiredRateLimits
usagerequiredUsage

MerchantInsight

FieldTypeDescription
namerequiredstring
categorystring | null
logo_urlstring | null
domainstring | null
countrequiredinteger
amountrequirednumber
averagerequirednumber
sharerequirednumber
first_daterequiredstring
last_daterequiredstring

Meta

FieldTypeDescription
api_versionrequiredstring
contract_versionrequiredstring
engine_versionrequiredstring
environmentrequiredstring
merchant_countrequiredintegerMerchants with a full profile.
alias_countrequiredintegerDistinct name variants the engine recognises.
categoriesrequiredarray of CategoryCount
rate_limitsrequiredRateLimits
max_batch_sizerequiredinteger

MonthInsight

FieldTypeDescription
monthrequiredstringYYYY-MM
countrequiredinteger
spendingrequirednumber
transfers_outrequirednumber
creditsrequirednumber
netrequirednumber

PayeeInsight

FieldTypeDescription
payee_idrequiredstring
namerequiredstringThe fullest spelling seen, capitalisation normalised.
aliasesrequiredarray of stringOther spellings of the same person seen on the statement.
banksrequiredarray of string
channelsrequiredarray of string
sentrequirednumberTotal you sent them.
receivedrequirednumberTotal they sent you.
sent_countrequiredinteger
received_countrequiredinteger
first_daterequiredstring
last_daterequiredstring
typical_sentnumber | nullMedian amount you send them.
typical_interval_daysnumber | nullMedian days between transactions with them (needs 3 or more).
regularrequiredbooleanThree or more transactions across at least two months.
relationshiprequiredstringyou pay them, they pay you or two-way.

RateLimits

FieldTypeDescription
per_minuterequiredinteger
per_dayrequiredinteger
statements_per_hourintegerStatement analyses (file uploads or /transactions/analyze calls) per hour.

Reconciliation

FieldTypeDescription
methodstring | nullrunning_balance: every row was checked against the statement's balance column.
checkedintegerRows that could be checked against the running balance.
matchedintegerRows whose amount and direction agree with the running balance.
match_ratenumber | nullmatched / checked. At or above 0.98 the parse is almost certainly right.
opening_to_closing_differencenumber | nullopening balance + credits - debits - closing balance. 0 when the statement adds up.
totals_matchboolean | nullTrue when opening + credits - debits equals the closing balance.

RecurringInsight

FieldTypeDescription
namerequiredstring
categorystring | null
logo_urlstring | null
frequencyrequiredstringweekly, fortnightly, monthly, quarterly or yearly.
occurrencesrequiredinteger
typical_amountrequirednumber
amount_variesrequiredbooleanTrue for bills whose amount changes month to month (electricity, phone).
totalrequirednumber
first_daterequiredstring
last_daterequiredstring
next_expectedrequiredstring
monthly_costrequirednumberTypical amount converted to a per-month figure.
confidencerequiredstring
likely_subscriptionrequiredboolean

RegularTransfer

FieldTypeDescription
directionrequiredstringdebit (you send) or credit (you receive).
amountrequirednumber
countrequiredinteger
totalrequirednumber
first_daterequiredstring
last_daterequiredstring
typical_interval_daysnumber | null
share_of_transfersrequirednumberShare of that direction's transfers that have this exact amount.

Revoked

FieldTypeDescription
revokedrequiredboolean

StatementInfo

FieldTypeDescription
formatrequiredstringcsv, xlsx, pdf or json.
filenamestring | null
currencyrequiredstring
transactionsrequiredintegerTransactions extracted.
rows_readrequiredinteger
rows_skippedrequiredintegerRows that were not transactions (totals, headers, notes) or could not be read.
confidencerequiredstringHow much to trust the parse: high, medium or low. high means the statement's own balances check out.
warningsrequiredarray of stringHuman-readable problems found while parsing. Show them to the user.
period_startstring | null
period_endstring | null
reconciliationReconciliation | nullSelf-check against the statement's balances. null when the statement has no balance column.

StatementTransaction

FieldTypeDescription
indexrequiredintegerPosition in the statement, starting at 0.
datestring | null
descriptionrequiredstring
amountrequirednumberAlways positive; see direction.
directionrequiredstringdebit (money out), credit (money in) or unknown.
balancenumber | null
kindrequiredstringWhat sort of transaction this is: merchant, transfer_in, transfer_out (person-to-person), cash, fee, income, interest, refund, bill_payment or unknown.
counterpartystring | nullThe other person in a transfer, when the narration names them (capitalisation normalised).
payee_idstring | nullStable id of that person within this statement: different spellings of one name share it.
counterparty_bankstring | nullThe other person's bank or wallet, when shown.
channelstring | nullibft, raast, wallet, transfer or card.
identifiedrequiredbooleanTrue 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.
enrichmentrequiredEnrichResultThe same result POST /v1/enrich returns for this narration.

Summary

FieldTypeDescription
currencyrequiredstring
transaction_countrequiredinteger
period_startstring | null
period_endstring | null
daysrequiredinteger
total_debitsrequirednumberAll money out, including transfers.
total_creditsrequirednumber
netrequirednumbertotal_credits - total_debits.
spendingrequirednumberMoney out excluding person-to-person transfers.
transfers_outrequirednumber
transfers_inrequirednumber
incomerequirednumberCredits recognised as salary/payroll.
average_daily_spendingrequirednumber
average_transactionrequirednumberAverage spending transaction.

TransactionIn

FieldTypeDescription
daterequiredstringTransaction date: 2026-01-31, 31/01/2026 (day first), 31-Jan-2026 and similar are understood. e.g. "2026-01-31"
descriptionrequiredstringThe narration exactly as the statement shows it. e.g. "POS/KHAADI PKG MALL LHR PK"
amountrequirednumber | stringAmount. A number (negative = debit when direction is omitted) or text such as "1,250.00 Dr" or "(1,250.00)". e.g. 1250.0
directionstring | nullIf you know it. Otherwise it is inferred from the sign, a Dr/Cr marker or the balance.
balancenumber | string | nullRunning balance after this transaction, if you have it. It makes direction detection and the self-check far more reliable.

TransactionsRequest

FieldTypeDescription
transactionsrequiredarray of TransactionIn
currencystring | nullISO currency code, used for labelling only.

TransferSummary

FieldTypeDescription
transfersrequiredinteger
namedrequiredintegerTransfers where the statement names the other person.
named_sharerequirednumberMany statements print only a reference, not a name: when this is low, regular_unnamed_transfers is the useful view.

UnidentifiedItem

FieldTypeDescription
descriptionrequiredstring
countrequiredinteger
amountrequirednumber

Usage

FieldTypeDescription
last_24hrequiredintegerTransactions 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_…"
  }
}
codeHTTPMeaning
invalid_request400The request body or a parameter is missing or malformed. details lists the offending fields.
missing_api_key401No API key was sent. Use Authorization: Bearer <key> or X-API-Key: <key>.
invalid_api_key401The API key is unknown or has been revoked.
not_found404No such endpoint.
method_not_allowed405That endpoint does not support this HTTP method.
payload_too_large413The request body is too large (1 MB for most endpoints; statements may be larger, see /v1/meta).
rate_limited429Too many requests. Wait Retry-After seconds, then retry.
unsupported_file415That file type isn't supported. Upload a PDF, CSV or Excel (.xlsx) statement.
statement_unreadable422The 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_error500Something 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

Categories

Accessories & Jewelry, App Stores & Gaming, Apparel, Auto, Banks & EMIs, Beauty & Wellness, Books & Stationery, Cafes & Coffee, Charity & Donations, Clubs, Courier & Logistics, E-commerce, Education, Electronics, Entertainment, Fitness, Footwear, Freelance & Ads, Fuel, Government & Tax, Groceries, Healthcare, Home & Furniture, Insurance, Investments & Savings, Kids & Baby, Lifestyle & Variety, Payment Aggregators, Pharmacy, Q-commerce & Delivery, Real Estate, Remittances & FX, Restaurants, Ride-hailing & Mobility, Sporting Goods, Sports & Recreation, Subscriptions & OTT, Telecom, Transit & Toll, Travel, Utilities - Electricity, Utilities - Gas, Utilities - Water

Data & privacy

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.

Machine-readable