Custom importer format
A custom importer definition maps parsed rows from a statement file into FinBodhi transactions. It is a portable JSON file intended for sharing, backup, and generation by tools such as LLMs.
Example
This complete CSV importer expects ::C0 to contain the date, ::C1 the description, ::C2 a negative debit amount, and ::C3 a credit amount.
{
"format": "finbodhi.importer",
"version": 1,
"template": {
"name": "Example Bank CSV statement",
"identification": {
"condition": {
"kind": "checkHeaders",
"headers": ["Date", "Description", "Debit", "Credit"]
},
"pdfOptions": null
},
"date": {
"column": { "kind": "column", "column": "::C0" },
"format": "dd/MM/yyyy"
},
"payee": {
"column": { "kind": "column", "column": "::C1" }
},
"account": {
"statementAccountId": null,
"isDebit": { "kind": "negative", "column": "::C2" },
"isCredit": { "kind": "positive", "column": "::C3" },
"otherAccountsName": {
"column": { "kind": "constant", "value": "Unknown" }
},
"otherAccountsCategory": "expenses",
"otherAccountsinstrumentType": "cash"
},
"fromQuantity": {
"column": {
"kind": "ifelse",
"if": { "kind": "blank", "column": "::C2" },
"then": { "kind": "column", "column": "::C3" },
"else": { "kind": "column", "column": "::C2" }
}
},
"fromPrice": {
"column": { "kind": "constant", "value": "1" }
},
"toQuantity": {
"column": {
"kind": "ifelse",
"if": { "kind": "blank", "column": "::C2" },
"then": { "kind": "column", "column": "::C3" },
"else": { "kind": "column", "column": "::C2" }
}
},
"toPrice": {
"column": { "kind": "constant", "value": "1" }
}
}
}
Envelope
{
"format": "finbodhi.importer",
"version": 1,
"template": {}
}
The envelope identifies the importer format and version. The template contains the rules that map parsed statement data to transactions. FinBodhi rejects unknown fields in both the envelope and the template. The supported version is 1.
Template
The template describes how each parsed row becomes a transaction. name gives the importer a descriptive label, while identification decides which template is used when a file is imported. The date, payee, fromQuantity, fromPrice, toQuantity, and toPrice sections map parsed-data columns to the corresponding transaction fields.
The account section is slightly different: it identifies the statement account and the other account involved in the transaction. Leave otherAccountsName as Unknown by default; account prediction can replace it with the appropriate account during import.
Operators
Importers use two distinct operator families: column rules produce a value from parsed data, while conditions produce true or false. A section only accepts the operator family documented for that section.
Column rules
A column rule can be one of four kinds: column, constant, arithmetic, or ifelse.
column
Use a column rule to read a value from one column in the parsed transaction table. In a custom importer, the column value must use ::C followed by a zero-based column number: ::C0 is the first column, ::C1 is the second, and so on.
{
"kind": "column",
"column": "::C0"
}
constant
Use a constant rule to return a literal string.
{
"kind": "constant",
"value": "1"
}
arithmetic
Use an arithmetic rule to combine two column rules. operator must be add, sub, mul, or div; lhs and rhs can each contain any column rule.
{
"kind": "arithmetic",
"operator": "div",
"lhs": { "kind": "column", "column": "::C2" },
"rhs": { "kind": "constant", "value": "100" }
}
ifelse
Use an ifelse rule to select between two column rules. if is a condition; then is used when it is true, and else when it is false.
{
"kind": "ifelse",
"if": { "kind": "negative", "column": "::C2" },
"then": { "kind": "column", "column": "::C2" },
"else": { "kind": "constant", "value": "0" }
}
Condition
A condition inspects one parsed-data column and returns true or false.
Conditions with column and value
These conditions use a string value to compare with the value in column:
containsandnotContainscheck whether the column contains a string.equalandnotEqualcheck whether the column exactly matches a string.startsWithandnotStartsWithcheck the start of the column value.endsWithandnotEndsWithcheck the end of the column value.
{ "kind": "contains", "column": "::C1", "value": "Card purchase" }
{ "kind": "notContains", "column": "::C1", "value": "Refund" }
{ "kind": "equal", "column": "::C0", "value": "DR" }
{ "kind": "notEqual", "column": "::C0", "value": "CR" }
{ "kind": "startsWith", "column": "::C1", "value": "UPI/" }
{ "kind": "notStartsWith", "column": "::C1", "value": "REV/" }
{ "kind": "endsWith", "column": "::C1", "value": "/FEE" }
{ "kind": "notEndsWith", "column": "::C1", "value": "/REV" }
Conditions with column
These conditions inspect the value in column without a value argument:
blankandnotBlankcheck whether the column is empty.positiveandnegativecheck whether the column is a positive or negative number.
{ "kind": "blank", "column": "::C3" }
{ "kind": "notBlank", "column": "::C3" }
{ "kind": "positive", "column": "::C2" }
{ "kind": "negative", "column": "::C2" }
Template sections
The following sections are all required unless a section explicitly permits null.
Name
name is a descriptive label for the importer. Keep it short and specific, such as the financial institution and statement type.
{
"name": "Example Bank statement"
}
Identification
When a file is imported, FinBodhi evaluates each template's identification.condition to decide which template to use. Identification supports only find, contains, and checkHeaders conditions.
find
Use find when text anywhere in the parsed file uniquely identifies the statement. Its value is a regular expression.
{
"condition": {
"kind": "find",
"value": "Example Bank Statement"
},
"pdfOptions": null
}
contains
Use contains when a particular parsed column must contain a string. The value is matched as literal text.
{
"condition": {
"kind": "contains",
"column": "::C0",
"value": "Transaction Date"
},
"pdfOptions": null
}
checkHeaders
Use checkHeaders for a delimited or spreadsheet file with a known header row. headers lists the expected header values in their column order.
{
"condition": {
"kind": "checkHeaders",
"headers": ["Date", "Description", "Amount"]
},
"pdfOptions": null
}
PDF options
pdfOptions controls PDF table extraction. Set it to null for every non-PDF importer. For a PDF importer, provide an object with all of these properties:
boundingBoxTemplate:nullor a normalized{ "x", "y", "width", "height" }bounding box applied to every page.pageBoundingBoxes: an array of{ "pageNumber", "boundingBox" }overrides for individual pages; use[]when there are none.byHorizontalLineandbyVerticalLine:true,false, ornull; enable a value when the table should be split on that type of line.skipFirstandskipLast: a page count ornull; use them to skip pages at the beginning or end of the PDF.
{
"condition": {
"kind": "find",
"value": "Example Bank Statement"
},
"pdfOptions": {
"boundingBoxTemplate": null,
"byHorizontalLine": false,
"byVerticalLine": true,
"pageBoundingBoxes": [],
"skipFirst": 0,
"skipLast": 0
}
}
All bounding-box values are normalized numbers. Set an unused optional PDF setting to null.
Date
The date section applies a column rule, then parses its result using format. format uses date-fns format tokens; use that reference for all supported formats. A row whose date.column value does not match format is ignored during import.
Common formats include:
dd/MM/yyyyfor31/01/2025MM/dd/yyyyfor01/31/2025yyyy-MM-ddfor2025-01-31dd-MM-yyyyfor31-01-2025dd.MM.yyyyfor31.01.2025dd MMM yyyyfor31 Jan 2025MMM dd, yyyyforJan 31, 2025dd/MM/yyfor31/01/25
{
"column": { "kind": "column", "column": "::C0" },
"format": "dd/MM/yyyy"
}
Payee
The payee section applies a column rule to produce the transaction description.
{
"column": { "kind": "column", "column": "::C1" }
}
Account
The account section determines the transaction's from and to accounts. When isDebit is true, the statement account is the from account and the other account is the to account. When isCredit is true, the other account is the from account and the statement account is the to account.
Always set statementAccountId to null in a custom importer definition. Account IDs belong only to the profile that created them. After the importer is added, the user can select the statement account in the UI.
otherAccountsName defines the generated other account. Use a constant Unknown by default so the account prediction engine can suggest the appropriate account during import. otherAccountsCategory and otherAccountsinstrumentType define that account's category and instrument type.
{
"statementAccountId": null,
"isDebit": { "kind": "negative", "column": "::C2" },
"isCredit": { "kind": "positive", "column": "::C2" },
"otherAccountsName": {
"column": { "kind": "constant", "value": "Unknown" }
},
"otherAccountsCategory": "expenses",
"otherAccountsinstrumentType": "cash"
}
fromQuantity
fromQuantity maps the quantity on the transaction's from account. For a cash transaction, use the statement amount.
fromQuantity, toQuantity, fromPrice, and toPrice always become positive values. Do not invert a negative source amount: FinBodhi determines the direction from the from and to accounts and applies the sign implicitly.
For a statement with separate debit and credit columns, use ifelse to select the populated amount:
{
"column": {
"kind": "ifelse",
"if": { "kind": "blank", "column": "::C2" },
"then": { "kind": "column", "column": "::C3" },
"else": { "kind": "column", "column": "::C2" }
}
}
fromPrice
fromPrice maps the price per unit on the transaction's from account. For ordinary cash transactions, use the constant "1".
{
"column": { "kind": "constant", "value": "1" }
}
For an instrument transaction, map the unit price from its own column instead:
{
"column": { "kind": "column", "column": "::C4" }
}
toQuantity
toQuantity maps the quantity on the transaction's to account. For a cash transaction, it is normally the same amount as fromQuantity; the imported quantity is converted to a positive number.
For a statement with one signed amount column, a direct column rule is enough:
{
"column": { "kind": "column", "column": "::C2" }
}
toPrice
toPrice maps the price per unit on the transaction's to account. For ordinary cash transactions, use the constant "1".
{
"column": { "kind": "constant", "value": "1" }
}
If a source has a price in ::C4 in cents for instrument transactions but leaves it blank for cash transactions, use ifelse to provide the cash default and convert the price:
{
"column": {
"kind": "ifelse",
"if": { "kind": "blank", "column": "::C4" },
"then": { "kind": "constant", "value": "1" },
"else": {
"kind": "arithmetic",
"operator": "div",
"lhs": { "kind": "column", "column": "::C4" },
"rhs": { "kind": "constant", "value": "100" }
}
}
}