# Let's Save standard import format (version 2) This page is for people and for AI assistants (Claude, ChatGPT and others) turning a bank statement, a card statement, or a whole history from another money app into something Let's Save can import. A file in this format imports with no mapping step: Let's Save recognises the header rows and reads every column the same way every time. - Human-readable guide: https://letssave.app/import-format - Excel template: https://letssave.app/import/template.xlsx (one sheet per part of the move; the "Example" sheets show filled-in rows and are never imported) - CSV template: https://letssave.app/import/template.csv (the Transactions header only) There are two jobs this format does: 1. **One statement**: a single account's transactions. Use the Transactions sheet alone (or the CSV template). 2. **A move from another app**: every account, category, transaction, transfer, scheduled payment and goal, brought across in one import that the person approves once. Use the whole workbook. ## The workbook - Excel `.xlsx`. The importer reads its sheets by name, in any letter case: `Transactions`, `Accounts`, `Categories`, `Recurring`, `Goals`. Sheets whose names start with `Example`, and the `Read me` sheet, are never imported. - A CSV file is one Transactions sheet. - On every sheet the first row is the header row, using exactly the column names below, lower case. Columns can be in any order. Leave out the optional columns you have nothing for. - One row per thing. Nothing else: no title rows, no totals, no blank rows between rows. - Dates are `YYYY-MM-DD`. Amounts use a `.` as the decimal point and at most 2 decimals, with no currency symbol, no thousands separator and no spaces. Let's Save processes a move in this order, and you should fill the sheets in the same order: **accounts first, then categories, then transactions and transfers, then scheduled payments and goals.** ## Accounts One row per account the person had in the old app, including closed ones that still have history. Fill this sheet first: every other sheet refers to accounts by their `name`. | column | required | what goes in it | |---|---|---| | `name` | yes | The account's name, unique within the workbook, e.g. `Everyday` or `Visa 4321`. Other sheets use exactly this name. | | `type` | yes | One of `bank`, `cash`, `card` (a credit card), `investment`, `loan`. | | `currency` | yes | The account's ISO 4217 currency code, e.g. `USD`, `EUR`, `LKR`. | | `closing_balance` | yes | Today's balance as the old app shows it (or the balance on `closing_date`). Negative for a card or loan that owes money. Ask the person if the export does not include it; never work it out from the rows, because it is the number the import is checked against. | | `closing_date` | no | The date `closing_balance` is from, when it is not today. | | `opening_balance` | no | The balance before the account's first transaction, as the old app shows it (its "initial" or "starting" balance). With it, Let's Save checks that `opening_balance` plus every row equals `closing_balance`. | | `last4` | no | The last 2 to 4 digits of the account or card number. Never more. | ## Categories One row per category or subcategory the old app used. Let's Save reuses a category the person already has with the same name and type, and creates the rest. The person sees which is which before anything is saved. | column | required | what goes in it | |---|---|---| | `name` | yes | The category's name, as the old app wrote it. Never `Transfer`: a transfer is not a category (see below). | | `type` | yes | `income` or `expense`. A category used for both gets two rows, one of each type. | | `parent` | no | For a subcategory, the name of its parent category. The parent needs its own row, with the same type and no parent of its own. | ## Transactions One row per transaction. The same columns as version 1, plus `transfer_id`. | column | required | what goes in it | |---|---|---| | `date` | yes | The date the money moved, written as `YYYY-MM-DD`, e.g. `2026-03-01`. Use the transaction date, not the posting date, when the statement shows both. | | `description` | yes | The statement's text for the transaction, as printed. At most 200 characters. | | `amount` | yes | A signed number with a `.` as the decimal point and at most 2 decimals. Money out is negative (`-54.20`), money in is positive (`3250.00`). No currency symbol, no thousands separator, no spaces. Never `0`. | | `currency` | no | The ISO 4217 code (`USD`, `EUR`, `LKR`...) when the row was paid in a different currency from its account's. Leave empty otherwise. | | `account` | no | The account's name from the Accounts sheet. Required whenever the workbook has more than one account. For a single statement, leave it out. | | `category` | no | A category from the Categories sheet, or one the person already uses in Let's Save, as `Name` or `Parent/Child` for a subcategory, e.g. `Food/Groceries`. Leave it empty when unsure: an empty category goes to "Other", where the person can recategorise it. Never on a transfer row. | | `payee` | no | The merchant or counterparty, cleaned up (`Green Market` rather than `GREEN MKT 0423 COLOMBO`). | | `notes` | no | Free text. | | `labels` | no | Tags separated by `;`, e.g. `work;travel`. | | `pending` | no | `true` for a transaction the statement marks as pending or not yet cleared. Leave empty otherwise. | | `reference` | no | The statement's own transaction id or reference number, when it prints one. Unique within an account. Used to skip exact duplicates when the same statement is imported twice. | | `transfer_account` | no | For money moved between the person's OWN accounts: the other account's name, e.g. `Savings`. Leave empty for ordinary spending and income. | | `transfer_id` | no | The same short id on both rows of one transfer, e.g. `T-17`, when the export has a row in each account. It ties the two rows together exactly. | ### Transfers Money moved between the person's own accounts is a **transfer**, never income or spending: paying off a credit card, moving money to savings, withdrawing cash, funding an investment or a fixed deposit, and the closing proceeds of one. - Every transfer row has `transfer_account` (or a `transfer_id` shared with its other row) and **no** `category`. Writing a transfer as income in one account and spending in the other inflates every report and is the most common mistake in a move. - When the old app's export has **both rows** of a transfer (one leaving an account, one arriving in another), keep both and give them the same `transfer_id`. They become one transfer. Across currencies, each row keeps its own account's amount. - When the export has **one row** per transfer, write it once, on the account the money left, with a negative amount and `transfer_account` naming the account it went to. - A `transfer_id` appears exactly twice: in two different accounts, one negative and one positive, the same amount when the currencies match, at most 31 days apart. A fee on a transfer is its own row. - Money sent to or received from someone else (a friend, a landlord) is not a transfer between the person's accounts: it is an expense or income row with no `transfer_account`. ## Recurring Payments the old app had scheduled: rent, salary, subscriptions, a monthly move to savings. Each becomes a recurring item that starts on its next date, so nothing already in the transactions is posted twice. Past occurrences belong in Transactions. | column | required | what goes in it | |---|---|---| | `account` | yes | The account's name from the Accounts sheet. For a transfer, the account the money leaves. | | `type` | yes | `income`, `expense` or `transfer`. | | `amount` | yes | A positive number, in the account's currency. | | `frequency` | yes | `daily`, `weekly`, `monthly` or `yearly`. | | `next_date` | yes | The next date it is due, after today, as `YYYY-MM-DD`. | | `interval` | no | Every how many periods, e.g. `2` with `weekly` for every two weeks. Defaults to 1. | | `day_of_month` | no | For monthly items, the day it falls on, 1 to 31, or `-1` for the last day. | | `description` | no | What the person will see it called, e.g. `Rent`. | | `category` | no | As in Transactions. Not for a transfer. | | `transfer_account` | no | For a `transfer`: the account the money goes to. Required for a transfer. | | `amount_to` | no | For a transfer between accounts in different currencies: what arrives, in the other account's currency. | ## Goals Savings goals from the old app. | column | required | what goes in it | |---|---|---| | `name` | yes | The goal's name. | | `target_amount` | yes | A positive number, in the person's main (base) currency. | | `target_date` | yes | When the person wants to reach it, as `YYYY-MM-DD`. Ask the person if the old app has none. | | `saved_amount` | no | How much the old app says is saved towards it, in the base currency. | | `account` | no | The account the goal saves in, from the Accounts sheet. The goal then follows that account's balance, starting from `saved_amount`. | | `start_date` | no | When saving towards it began. | ## Rules for extracting a statement or an export 1. **One row per transaction.** Skip opening and closing balances, running-balance columns, subtotals, page headers and footers, interest summaries and anything that is not a transaction. 2. **Signs.** Write money leaving the account as a negative amount and money arriving as a positive one, whatever convention the statement uses. On a credit card statement, a purchase is negative and a payment to the card is positive. 3. **Dates.** Convert every date to `YYYY-MM-DD`. If the statement's year is only in its header, apply it to every row, and watch for statements that cross a year end. 4. **Never invent.** Do not guess a category, a payee or a reference the source does not support. An empty cell is always fine. Ask the person for what only they know: today's balances, goal dates. 5. **Check before handing over.** For every account, `opening_balance` plus the sum of its amounts (counting each transfer on both accounts) should equal `closing_balance`. If it does not, a row is missing, doubled or has the wrong sign, or a transfer is written as income and spending: find it before giving the workbook to the person. 6. **Privacy.** Never put a full card or account number in any cell. At most the last 4 digits, e.g. `Visa 4321`. ## Moving someone over from another app 1. **Inventory the export.** List every account (with its type, currency, and today's balance in the old app), every category and subcategory, which rows are transfers between the person's own accounts, every scheduled payment and every goal. Ask the person for any balance the export lacks. 2. **Accounts.** One Accounts row each, with `closing_balance` and, when the old app shows it, `opening_balance`. 3. **Categories.** One Categories row each, keeping the old app's names. Leave out "Transfer"-style categories: those rows are transfers. 4. **Transactions and transfers.** Every row, with its `account`. Mark every transfer as described above. 5. **Recurring and goals**, if the old app had them. 6. **Check every account adds up** (rule 5). Then hand over the workbook, or send it through the connector. ## Getting it into Let's Save **Upload the file.** The person opens Import in the app (https://app.letssave.app/app/import). A statement goes in with "Add a statement"; a workbook with an Accounts sheet goes in with "Move from another app". The map step is skipped. They review the accounts, categories, duplicates and transfers, then import everything at once. Every import can be undone for 30 days. **Or send it through the Let's Save connector.** If the person has connected you to Let's Save (Settings > AI connections), do not write a file. Call `letssave.guide` first if you have not. - *One statement:* call `letssave.transactions.propose_import` with the account id (from `letssave.accounts.list`) and the Transactions rows as JSON, with the same field names and the same rules as above. Instead of an `account` name, a row that belongs to another of the person's accounts carries that account's `accountId`; `transfer_account` must be one of the person's account names. - *A move from another app:* never propose accounts, categories or transactions one by one. Build one draft, in this order: 1. `letssave.migration.setup` with the Accounts and Categories rows. It says which accounts and categories will be created and which already exist. 2. `letssave.migration.add_transactions`, once per part of up to 500 Transactions rows, numbering the parts from 0. Sending a part again replaces it. 3. `letssave.migration.add_extras` with the Recurring and Goals rows, if any. 4. `letssave.migration.check`. It runs Let's Save's own analysis and reports, per account, whether the rows add up, and which transfers paired, which rows look like duplicates and which look like a transfer written twice. Fix what it reports and check again until it is clean. If it says the analysis is still running, call it again. 5. `letssave.migration.submit`. This creates one request; the person reviews and imports the whole move in the app, once. If the check left warnings, show them to the person before submitting. 6. After the person imports, `letssave.proposals.status` with the request id returns the result: what was created, and each account's balance against the source. Report it to the person. In JSON, a value left out and an empty string mean the same thing. Nothing is saved until the person reviews and approves the import in the app. Tell them that plainly and give them the link the tools return. ## Examples Transactions: ```csv date,description,amount,currency,account,category,payee,notes,labels,pending,reference,transfer_account,transfer_id 2026-03-01,Salary March,3250.00,,Everyday,Salary,Acme Ltd,,,,TXN-0001,, 2026-03-02,Groceries,-54.20,,Everyday,Food/Groceries,Green Market,,weekly;food,,TXN-0002,, 2026-03-03,Coffee,-3.80,,Everyday,,Corner Cafe,,,true,TXN-0003,, 2026-03-05,Move to savings,-200.00,,Everyday,,,Monthly saving,,,TXN-0004,Savings,T-1 2026-03-05,From Everyday,200.00,,Savings,,,,,,,Everyday,T-1 2026-03-12,Cinema,-45.00,,Visa,Fun,City Cinema,,,,,, ``` Accounts (each adds up: Everyday 1500.00 + 3250.00 − 54.20 − 3.80 − 200.00 = 4492.00): ```csv name,type,currency,closing_balance,closing_date,opening_balance,last4 Everyday,bank,USD,4492.00,2026-03-31,1500.00,4321 Savings,bank,USD,5200.00,2026-03-31,5000.00, Visa,card,USD,-45.00,2026-03-31,0.00,9876 ``` Categories: ```csv name,type,parent Salary,income, Food,expense, Groceries,expense,Food Fun,expense, Rent,expense, ``` Recurring: ```csv account,type,amount,frequency,next_date,interval,day_of_month,description,category,transfer_account,amount_to Everyday,expense,1200.00,monthly,2027-01-01,,1,Rent,Rent,, Everyday,transfer,200.00,monthly,2027-01-05,,5,Monthly saving,,Savings, ``` Goals: ```csv name,target_amount,target_date,saved_amount,account,start_date Emergency fund,10000.00,2027-12-31,5200.00,Savings,2026-01-01 Holiday,2500.00,2027-06-30,300.00,, ```