Documentation
Get your Indian bank statements into Actual Budget, with real payee names instead of UPI reference strings.
Why this exists
Section titled “Why this exists”1. Actual has no bank sync for Indian banks, and is unlikely to get one soon. (more below). The only way to get transactions in is to download a statement and import it yourself. This tool makes that as painless as it can be: hand it the file your bank gave you (CSV, Excel or PDF) and it either produces a clean CSV for Actual or puts the transactions straight into your budget.
2. UPI transactions are unreadable as they come. Your bank describes two orders from the same shop like this:
UPI/DR/412345678901/SWIGGY/YESB/swiggy@ybl/PaymentUPI/DR/419876543210/SWIGGY/YESB/swiggy@ybl/PaymentEvery line has a different reference number, so Actual treats them as two different payees. A few months in, you have hundreds of junk payees, reports by payee tell you nothing, and Actual cannot learn your categories.
This tool pulls the name out of each line, so both become simply Swiggy:
| Date | Payee | Notes | Amount |
|---|---|---|---|
| 2024-04-01 | Swiggy | UPI/DR/412345678901/SWIGGY/YESB/swiggy@ybl/Payment |
-450.50 |
| 2024-04-03 | ATM Withdrawal | ATW/1234/CASH WDL/BANGALORE |
-2000.00 |
| 2024-04-04 | DMart | POS 1234XXXX5678 DMART BANGALORE |
-3250.75 |
| 2024-04-05 | John Doe | UPI/412345678903/JOHN DOE/johndoe@oksbi |
1500.00 |
The bank’s original text is kept in Notes, so nothing is lost.
Quick start
Section titled “Quick start”You need Node.js 22.14 or newer. There is nothing else to install.
Step 1. Download a statement from your bank. Use internet banking rather than the mobile app if you can: the website usually offers Excel or CSV, which work better than PDF.
Step 2. Convert it.
npx india2actual statement.csvThis writes statement.actual.csv next to your file. Works the same for
.xls, .xlsx and .pdf.
Step 3. Import it into Actual. Open the account, choose Import, pick
statement.actual.csv, and match up the Date, Payee, Notes and Amount
columns. Leave Reference unmapped. Actual remembers this per account, so you
only do it once.
That’s it.
If you use it often, install it so you can drop the npx:
npm install -g india2actualSkip the import step
Section titled “Skip the import step”If you run an Actual sync server, the tool can send transactions straight into your budget. No CSV, no import dialog.
Create a file named .env in the folder you run the tool from:
ACTUAL_SERVER_URL=https://actual.example.comACTUAL_PASSWORD=your-actual-passwordACTUAL_SYNC_ID=your-sync-id # Actual: Settings > Advanced > Sync IDPreview first, then do it for real:
india2actual statement.pdf --push --account "ICICI Savings" --dry-run# [dry run] ICICI Savings: would add 34, would update 0.
india2actual statement.pdf --push --account "ICICI Savings"Pushing is also the better way to import:
- No duplicates. If two statements overlap, transactions already in Actual are skipped.
- Actual learns faster. It sees both the clean payee and the bank’s original text, which is what its payee matching learns from.
Want to check the result before it reaches your budget? Convert, edit the CSV, then push the CSV:
india2actual statement.pdf# open statement.actual.csv and fix anything you likeindia2actual statement.actual.csv --push --account "ICICI Savings"Password-protected PDFs
Section titled “Password-protected PDFs”Add the PDF’s password to your .env file:
STATEMENT_PASSWORD=your-pdf-passwordPasswords go in .env rather than on the command line so they do not end up in
your shell history.
Fixing a payee name
Section titled “Fixing a payee name”Common Indian merchants are recognised out of the box. For anything else, such as your local shop or your employer, write your own rules in a JSON file:
[ { "pattern": "^mylocalkirana", "name": "Kirana Store" }, { "pattern": "^acmecorp", "name": "Acme Payroll" }]india2actual statement.csv --merchants my-merchants.jsonWrite patterns in lowercase with no spaces or dots. They are matched against
the UPI ID (the part before the @) or the merchant name. Your rules win over
the built-in ones.
Two cases where rules are especially useful:
-
EMIs and other auto-debits (NACH). The bank’s text has no name, only a mandate number, so these show up as something like
NACH ICIC0000000000000001. Name each one once:[{ "pattern": "icic0000000000000001", "name": "Home Loan EMI" }] -
Names cut short. Some banks trim names to about ten characters, so the same person can appear as
Mr A N OTHE,A N OTHERorOTHER. Add one rule per variant to merge them.
It checks its own work
Section titled “It checks its own work”Nearly every Indian statement has a running balance column. The tool uses it to verify every transaction: each amount must match the change in balance. If anything does not add up, it refuses to write the file and tells you which row is wrong:
Balance check FAILED (4/5 rows agree). 1 of 5 rows do not agree with the balance column. 2024-04-03 "ATW/1234/CASH WDL/BANGALORE": balance moved by -2000.00 but the parsed amount is -9000.00Refusing to write a statement that does not reconcile. Re-run with --force to write it anyway.Credit card statements
Section titled “Credit card statements”Credit card statements work the same way: run the tool on the file and import the result into a credit card account in Actual.
npx india2actual card-statement.pdfThere is no running balance on a card statement, so the check is different. If the statement prints its totals for the period (purchases and payments), the tool adds up the rows it read and refuses to write the file if either total is off:
Statement totals check FAILED (1/2 totals agree). Purchases and cash advances: the statement says 3210.40 but the parsed rows total 2500.00Refusing to write a statement that does not reconcile. Re-run with --force to write it anyway.Statements that do not print totals, such as a yearly summary, cannot be
checked, and the tool says so. If a card statement is not recognised and the
signs come out reversed, add --card.
So far this has been tested on ICICI card statements only. Other issuers’
conventions (a trailing C or D, a leading + on credits, DR and CR
markers, and summary lines for finance charges and fees) are handled from public
documentation of those layouts and are unverified.
Card payments as transfers
Section titled “Card payments as transfers”A card payment shows up twice: as an autopay debit on your bank statement and as
a payment on your card statement. Imported as ordinary transactions, one looks
like spending and the other like income. With --push, the tool can link them
as a transfer instead:
# card statement: the payment becomes a transfer with your bank accountindia2actual card.pdf --push --account "ICICI Amazon Pay" --transfer-to "ICICI Savings"
# bank statement: the autopay debit becomes a transfer with the cardindia2actual savings.pdf --push --account "ICICI Savings" --transfer-to "ICICI Amazon Pay"Use the option on both runs, in either order. Whichever statement you import first creates the transfer, and the other one matches it instead of adding a second transaction.
- It only works with
--push. Actual’s CSV import has no way to create transfers. - Only payments the tool recognises as card payments are affected. Everything else is imported as usual.
- The two amounts must match, with dates within 7 days. A minimum-due payment that differs from the autopay amount will not pair up.
- If the other account already has an ordinary transaction for a payment, for example from an earlier import without this option, that payment is imported normally and the tool says so. Link that pair in Actual yourself, since the API cannot link existing transactions.
- With several cards, name the right card on each run.
Tested against Actual 26.9.
Reference
Section titled “Reference”Supported files
Section titled “Supported files”| File | Supported |
|---|---|
| CSV / TSV | yes |
Excel .xlsx |
yes |
.xls that is really HTML or XML |
yes |
| PDF, including password-protected | yes |
Old binary .xls |
no, re-save as .xlsx or CSV |
Supported banks
Section titled “Supported banks”| Bank | Account | Formats | Status |
|---|---|---|---|
| ICICI | Savings | Tested | |
| ICICI | Credit card | PDF (annual and monthly Amazon Pay layouts) | Tested |
| Federal Bank | Savings | Tested | |
| IDFC FIRST Bank | Savings | Excel | Tested |
| CSB Bank | Savings | CSV | Tested |
| HDFC | Savings | CSV, Excel | Unverified |
| SBI, Axis, Kotak, PNB, Bank of Baroda, Canara, IndusInd, Yes Bank | Savings | CSV, Excel, PDF | Unverified |
| HDFC, SBI, Axis, Kotak, IndusInd | Credit card | Unverified |
Tested means a real statement was run and the output checked. Unverified means the bank’s usual layout is expected to work, but nobody has confirmed it on a real file. If you try one, a report of what worked or failed is very welcome. The most useful report is the header row and a few narrations with names, numbers and references replaced by fakes. Please never share a real statement.
Options
Section titled “Options”| Option | Purpose |
|---|---|
--out <path> |
Where to write the CSV. Default <input>.actual.csv. |
--stdout |
Print the result instead of writing a file. |
--date-order dmy|mdy|ymd |
Only affects all-numeric dates, where 01/02/2024 is ambiguous. Default dmy. |
--delimiter <char> |
Force the CSV delimiter instead of detecting it. |
--merchants <path> |
Your own payee rules. See above. |
--card |
Read the file as a credit card statement. Normally detected automatically. |
--env-file <path> |
Read settings from this file instead of ./.env. |
--push |
Send to Actual directly instead of writing a CSV. |
--account <name|id> |
Which Actual account to import into. Required with --push. |
--dry-run |
With --push, report what would change without writing. |
--transfer-to <account> |
With --push, send card payments as transfers with this account. See above. |
--force |
Write even if the balance check fails. |
--quiet |
Only report problems. |
Settings
Section titled “Settings”These go in a .env file in the folder you run the tool from. None are needed
just to convert a statement to CSV.
| Setting | Purpose |
|---|---|
STATEMENT_PASSWORD |
Password on an encrypted statement PDF. |
ACTUAL_SERVER_URL |
Your Actual sync server. |
ACTUAL_PASSWORD |
Password for that server. Not the statement password. |
ACTUAL_SYNC_ID |
Budget to import into: Settings > Advanced > Sync ID. |
ACTUAL_ENCRYPTION_PASSWORD |
Only for end-to-end encrypted budgets. |
ACTUAL_DATA_DIR |
Local budget cache. Default ./.actual-cache. |
Real environment variables override the file, which is handy for a one-off:
ACTUAL_SYNC_ID=other-budget india2actual statement.pdf --push --account SavingsHow duplicates are avoided
Section titled “How duplicates are avoided”Each UPI transaction carries a 12-digit reference number. The tool writes it to
the Reference column, and with --push Actual uses it to recognise a
transaction it already has.
Only exactly-12-digit values that appear once in the file are used. Anything else could be an account or card number, and would make Actual wrongly merge separate transactions. Where there is no reference, Actual falls back to matching on date and amount.
When you push a CSV you converted earlier, payees you edited are kept as they are and the references survive. The balance check cannot run again, because the converted CSV has no balance column, but it already ran when the CSV was made.
Limitations
Section titled “Limitations”- PDF is the least reliable input. The table has to be rebuilt from where each piece of text sits on the page, which can break when a bank changes its layout. The balance check is there to catch this. Use CSV or Excel when your bank offers it.
- A card statement whose spending equals its payments cannot reveal reversed signs through the totals check, since both totals swap to the same figure. Detection normally gets the signs right, so this only matters if a card statement is not recognised.
- Long text in a PDF may gain or lose a space where a line wrapped. Dates, amounts, payees and references are not affected.
- Payee names are a best guess. Payment gateways often hide the real shop. You get one consistent payee instead of one per transaction, but not always the actual shop.
- It cannot fetch statements for you. India’s Account Aggregator framework needs a registered entity and a commercial contract, so a free always-on connector like Actual’s European bank sync is not possible. You download the statement; this tool does the rest.
Development
Section titled “Development”git clone https://github.com/emilgeo/india2actual.gitcd india2actualnpm install
npm testnpm run typechecknpm run dev -- statement.csv # run the CLI from sourceLicense
Section titled “License”MIT