Skip to content
investiDocs

Import your history

Accepted Schwab CSV, Schwab JSON, generic CSV, positions CSV and Order Status CSV formats, how duplicates are skipped, and how to delete an import batch.

On this page

Imports are the most complete way to bring in your record. Investi's Schwab sync asks for the last 365 days of trades, while a Schwab History export can go back as far as Schwab keeps it. Every row gets a fingerprint, so importing the same or an overlapping file twice never duplicates anything.

Where to import#

Open Settings and find the Import Schwab history panel (/settings#data). Choose Transactions, Positions or Orders, pick an existing account or type a name for a new one, then drop the file or click to choose it.

  • Accepted: .csv and .json files up to 15 MB.
  • Rejected: .xlsx and .pdf statements. In Schwab, use History > Export (CSV or JSON) instead.

The same upload is available as POST /api/v1/imports (multipart form with file, kind of transactions, positions or orders, and either accountId or accountName). It needs the workspace:write scope. See the REST API.

How the format is detected#

For a transactions import, Investi decides the format from the file itself:

  1. Schwab JSON when the filename ends in .json or the content starts with {.
  2. Schwab CSV when any row contains cells named exactly Date and Action (case does not matter). Title rows above the header are skipped, and rows whose date cell contains "total" are ignored.
  3. Generic CSV otherwise, with the header in the first row.

Schwab transactions CSV#

Export from schwab.com: Accounts > History > Export. Both the current and the legacy layouts work. Columns are matched by name:

Column Matched by
Date exact name
Action exact name
Symbol exact name
Description exact name
Quantity exact name, or any name starting with Qty
Price exact name
Fees any name containing Fees, such as Fees & Comm
Amount exact name
CSV
"Date","Action","Symbol","Description","Quantity","Price","Fees & Comm","Amount"
"10/30/2026 as of 10/29/2026","Buy","SPXC","SPX TECHNOLOGIES INC","10","$148.21","","-$1,482.10"
"10/31/2026","Sell","SPXC","SPX TECHNOLOGIES INC","10","$160.31","$0.05","$1,603.05"

Schwab transactions JSON#

The JSON export is an object with a BrokerageTransactions array. Each entry is read from the keys Date, Action, Symbol, Description, Quantity, Price, Fees & Comm and Amount.

JSON
{
  "BrokerageTransactions": [
    { "Date": "10/30/2026", "Action": "Buy", "Symbol": "SPXC", "Description": "SPX TECHNOLOGIES INC",
      "Quantity": "10", "Price": "$148.21", "Fees & Comm": "", "Amount": "-$1,482.10" }
  ]
}

Generic CSV#

Any CSV with a date column and a side column works. Header names are matched case-insensitively, and the first matching column wins:

Field Header rule Required
Date contains date Yes
Side starts with side, action, type or transaction Yes
Symbol starts with symbol or ticker For fills
Quantity starts with quantity, qty or shares For fills
Price starts with price For fills, unless an amount is present
Fees contains fee or commission No
Amount starts with amount, net amount or total No
Description starts with description, memo or note No

Sides B and S are read as Buy and Sell. Without a date and a side column, the import stops with "Could not find Date and Action/Side columns."

CSV
trade date,side,ticker,shares,price,commission
2026-01-02,B,AAPL,10,190.50,0
2026-01-20,S,AAPL,10,201.10,0

Note: A file whose headers are exactly Date and Action is read with the Schwab CSV rules above, which expect exact names like Symbol, Quantity and Price, and which do not translate B and S.

How rows are read#

Dates. MM/DD/YYYY (two-digit years get 2000 added) or ISO YYYY-MM-DD. For Schwab's "10/30/2026 as of 10/29/2026", the as-of date wins. Fills are stored at 4:00 PM Eastern on the trade date.

Numbers. $, commas, spaces and % are stripped; (12.00) and -$12.00 are negative; empty, -- and N/A mean no value. Fees are always taken as positive.

Price. When the price is blank, it is derived as the absolute amount divided by the absolute quantity. A fill with neither is reported as an error and skipped.

Actions.

Action text Becomes
Buy, Reinvest Shares, Buy to Open Buy that opens
Buy to Cover, Buy to Close Buy that closes a short
Sell, Sell to Close Sell that closes
Sell Short, Sell to Open Sell that opens a short
Stock Split, Forward Split Split (see below)
Security Transfer, Journaled Shares, Transfer of Security Shares in: a buy with unknown basis. Shares out: ignored with a warning, never treated as a sale.
Anything else with an amount A cash event

Cash events are classified in this order: margin interest, then dividends and capital gain distributions, then other interest, tax, fees (including ADR management fees), and deposits or withdrawals (MoneyLink, wire, ACH, journal and similar, by the sign of the amount). An amount that matches none of these is kept as "other" cash with a warning. A row with an unknown action and no amount is skipped with a warning.

Transfers in. Shares transferred in carry no cost basis. They enter the ledger at price 0 marked unknown basis, and any later sale of those shares is excluded from verified realized P/L. See The ledger.

Splits. A Schwab split row lists the shares added. Investi derives the ratio from the position it reconstructs from the same file: (shares before + shares added) / shares before. If the file holds no position before the split date, the ratio cannot be derived, a warning is recorded and the split is not applied.

Symbols. Every ticker is resolved against the SEC's list of registered tickers. Symbols that cannot be resolved are listed after the import, and their rows are not imported. Investi does not guess.

Duplicates#

Each row's fingerprint is a hash of its raw date, action, symbol, quantity, price, fees and amount text, plus an occurrence number so that two genuinely identical rows in one file stay distinct. Fingerprints are unique per account: re-importing the same or an overlapping file into the same account inserts only the rows that are new and counts the rest as duplicates.

Warning: Duplicate detection works within one account and compares raw text. Importing the same history into two different accounts, or importing a CSV into the account that Schwab sync created, counts those trades twice. Pick one format and one account per brokerage account.

When Schwab sync first sees a brokerage account, it reuses an account you created by import if exactly one such account (no Schwab link yet, not the sample) has a masked number matching the account number's last digits. Give imported accounts their last digits and the sync and your imports share one account. With no match, or more than one, the sync creates its own account.

Positions CSV#

A Schwab Positions export (Accounts > Positions > Export) gives exact share counts, cost basis, cash and margin. Choose Positions before uploading.

  • The header row is the first row whose first cell is Symbol. Other columns are matched by name: description, Qty or quantity, Price, Mkt Val or market value, Cost Basis, and security type.
  • The Cash row sets cash (a negative value is recorded as a margin debit) and the Account Total row sets equity.
  • Rows whose security type is not an equity, ETF, stock or fund are skipped with "is not an equity; other asset classes are not yet supported."

A positions import is a snapshot at the moment you upload it. Each upload adds a new snapshot rather than replacing or deduplicating the last one.

Order Status CSV#

Schwab's API returns orders only for accounts enabled for thinkorswim. For other accounts, export your orders from schwab.com: Trade > Order Status > Export, then choose Orders (or drop the file under any type; it is recognized from its header).

The header row is the first row with an order number column, a status column and a symbol column; title lines above it are read for the account's last digits. Columns are matched by name, never by position:

Field Matched by Required
Order number Order #, Order Number, Order No, Order ID Yes
Symbol Symbol or Ticker Yes
Status Status or Order Status Yes
Action Action, Side or Instruction Yes
Quantity Qty, Quantity or Shares Yes
Filled quantity Filled Qty, Qty Filled, Filled No
Price Price or Limit No
Stop Stop or Stop Price No
Order type Order Type or Type No
Timing Timing, Duration or Time in Force No
Entered a date and time column (Time and Date, Entered, Submitted) No
Expires a name starting with Expir No

A file without an order number column is refused: the order number is what keeps imported orders and later Schwab syncs on the same record, and Investi does not make one up.

Values. Status Open, Working, Received, Pending or Queued becomes working, or partially filled when some shares are filled; Filled becomes filled; Canceled or Replaced canceled; Expired expired; Rejected rejected; a pending cancel stays working until Schwab confirms it. A status Investi doesn't recognize is reported and the row skipped. Actions starting with Buy or Sell set the side. Price cells such as Limit $12.50, Stop $10.00, Stop Limit $10.00 / $9.90 or Market set the order type and prices. Quantity may be 100 or 50 of 100 (filled of total). Timing GTC, Day and an extended-hours flag set the duration and session. Times are read as New York time. Option and other non-stock symbols are skipped with a note.

Which account. The filename (for example ...XXXXX782_Order_Status_20261101-090901.csv) or the title line names the account's last digits. If you chose an account whose masked number disagrees, the import stops and asks you to pick the matching one. With + New account, an existing account ending in those digits is used instead of creating a duplicate.

Re-importing and Schwab sync. Orders are keyed by account and Schwab order number, the same key Schwab sync uses. Whichever arrives first, the other updates the same order rather than adding one. Each order's status is as of the time in the filename (20261101-090901, New York time), or the upload time when the file has none. A re-import updates status, filled quantity and prices only when the file is at least as new as what is stored, so an older export never rolls back a fill or cancellation that a later sync or export recorded. Orders in a file that match what is stored are counted as unchanged.

Verdict history#

If you already keep verdicts in a spreadsheet, bring them in so Investi knows what you have reviewed. Open Settings, Import verdict history (/settings#verdicts), and drop the file, or send it to POST /api/v1/imports/verdicts as the multipart field file.

Formats. An .xlsx workbook (the first sheet is read) or a .csv file, up to 10 MB. Older .xls, Numbers and ODS files are refused with a request to save as .xlsx or CSV. Workbooks are read with fflate 0.8.3, a small zip library with no dependencies, plus Investi's own reader for the sheet XML; the xlsx package on npm was not used because its last npm release (0.18.5) has unfixed security advisories, and exceljs pulls in about 20 MB of dependencies for features an import never needs.

Columns. The header row can sit anywhere in the first 15 rows. Names are matched loosely:

Column Header names accepted Required
Ticker Ticker, Symbol, Stock Yes
Verdict Verdict, Status, Rating, Signal, Color, Call, Grade Yes
Date Date, As of, Reviewed, Review date, Last reviewed, Updated No
Notes Notes, Comment, Comments, Rationale, Reason, Remarks, Thesis No

Verdicts are read case-insensitively: GREEN (also G, Go, OK, Pass, Approved), YELLOW (Y, Amber, Watch, Caution, Wait), RED (R, No, Avoid, Reject, Fail), DORMANT (D, Inactive, Parked, Asleep) and FLAGGED (F, Flag, Red flag). The colored circle emoji work too.

Dates can be YYYY-MM-DD, MM/DD/YYYY, MM/DD/YY, Oct 14, 2026, 14-Oct-2026 or an Excel date cell. A row without a date is dated at the import time. A future date is replaced by the import time, with a note.

Validation. Every row is either imported or listed with its reason: no ticker, a cell that is not a ticker, a ticker that is not a listed US security, an unknown verdict word or an unreadable date. Two rows for the same ticker and date keep the later row and list the earlier one as replaced. Nothing is dropped silently. The result shows how many were imported, how many were already imported, the count of each verdict and the columns used.

What it stores. Each row becomes an evaluation of that name marked source: imported, with the verdict as the override and the note as its rationale, dated from the row (noon New York time). Re-importing the same file writes nothing twice. Then:

  • Discover treats the name as reviewed: Unchanged while today's verdict matches yours, Changed once it differs, and never New. See Candidate states.
  • Your verdict stays current. A fresh analysis (for example opening the company page) records the engine's verdict beside it and never replaces it; this holds even when the name was analyzed after the review's date. The research page shows both, for example "Your verdict RED, imported Oct 12 · engine says YELLOW", and Discover marks the name Changed only when the engine's verdict moves while yours stays.
  • Clear imported verdict on the research page (or DELETE /api/v1/securities/{ticker}/override) writes a new revision without your verdict, so it stops carrying forward and the engine's verdict becomes current. Re-importing the same file later does not bring a cleared verdict back. Manual overrides carry forward and clear the same way.
  • The action list, the REST report (userVerdict) and the MCP tools use your verdict as the current one.

A sample file is at /samples/verdict-tracker-sample.csv.

Import batches#

Every upload creates an import batch that records the filename, detected format, row count, rows inserted, duplicates skipped, up to 500 issues, and the coverage window (earliest and latest dated row). The Imports panel on the History page lists your 20 most recent batches, and GET /api/v1/imports returns all of them.

After an upload the panel shows the result: format, new rows, duplicates skipped, unresolved symbols, coverage, and any notes from the import.

Deleting a batch#

There is no delete button in the app yet. Send DELETE /api/v1/imports/{id} with a token that has workspace:write:

Shell
curl -X DELETE https://investistocks.vercel.app/api/v1/imports/imp_abc123 \
  -H "Authorization: Bearer $INVESTI_TOKEN"

Deleting a batch removes, in one transaction, the fills and cash events it inserted, the position snapshots from a positions batch, the orders an orders batch holds (an order belongs to the newest import that listed it), and the batch record. It does not remove:

  • rows that were skipped as duplicates, because those belong to the earlier batch that inserted them,
  • splits, which are stored once per security and shared,
  • the balance snapshot written by a positions import,
  • the account itself.