Skip to main content

Preparing your CSV

The column-by-column reference for the Bulk Import template. Covers every header in order, required vs. optional fields, the two product-matching paths, date format, USD-only pricing, dealer handling, and where each row lands.

The Bulk Import template is a 14-column CSV. Most of the columns are optional — a well-formed row can be as short as five filled fields.

This article walks through every column in the template, the formats it expects, and the small handful of rules that turn a spreadsheet into a clean import. Pair it with the overview if you want the conceptual context: see Bulk import overview.

Get the template by clicking Download template CSV at the top of the Bulk Import page. It comes pre-headed and includes a few sample rows you can replace with your own data.

The columns at a glance

The template has 14 columns, in this order. Keep the headers as-is — the import matches on column name, not position, but the safest path is to leave the layout alone.

  • date: Required. The date of the purchase.

  • catalog_id: Optional. The product key for a standard catalog item or one of your saved custom items. Recommended where possible.

  • product_name: Required only when catalog_id is blank. The name of the custom item.

  • metal: Required only when catalog_id is blank. One of GOLD, SILVER, PLATINUM, PALLADIUM. Lowercase is accepted, uppercase is the canonical form.

  • form: Required only when catalog_id is blank. One of COIN, BAR, ROUND, JUNK. Lowercase is accepted, uppercase is the canonical form.

  • weight_oz: Required only when catalog_id is blank. The per-unit weight in troy ounces.

  • purity: Required only when catalog_id is blank. A decimal greater than 0 and less than 1 — 0.9999 for fine gold, 0.999 for fine silver.

  • quantity: Required. How many units you bought on this row, as a positive whole number.

  • price_per_unit: Required. What you paid per coin or bar, in USD.

  • premium_per_unit: Optional. Per-unit premium over spot, in USD.

  • spot_at_time: Optional. Spot price per troy ounce at the time of purchase, in USD.

  • shipping_cost: Optional. Total shipping for the row, in USD.

  • dealer: Optional. Free-text dealer name.

  • notes: Optional. Free-text notes attached to the transaction.

The rest of this article unpacks each column in detail, in the order it appears in the template.

date (required)

The purchase date for the row. Use the ISO format YYYY-MM-DD — that's what the template documents and what we test against. Examples: 2024-07-15, 2023-11-30.

A few specifics worth knowing:

  • Use ISO. Don't get clever. Other date formats may technically parse when you open the file in Excel, but ISO is the only format we guarantee.

  • Future dates aren't allowed. Same rule as the Record Purchase form — the import flags rows whose date is in the future.

  • The date drives short-term vs. long-term treatment if you sell those items down the line, so getting it right matters more than it might feel like at upload time.

Product matching: catalog_id versus the five custom fields

Every row has to identify exactly one product. The template gives you two routes — pick one per row.

Path A — Use a catalog_id (recommended)

Put the product key in the catalog_id column and leave product_name, metal, form, weight_oz, and purity blank on that row.

  • For standard catalog products, the key is the slug shown on the Standard Bullion tab of the Catalog page. Examples: american-gold-eagle-1oz, silver-maple-leaf-1oz, pre-1965-dime.

  • For your saved custom items, the key is the CSV Key shown on the My Custom Items tab. It's auto-generated when you create the custom product, and you can copy it from the Catalog page.

The import matches the key exactly — capitalization and hyphens included. A typo like american-gold-eagle-1z (missing the o) flags the row.

Path B — Leave catalog_id blank and describe the product inline

If a product isn't in the standard catalog and you haven't pre-built a custom item for it, you can describe it on the row itself. Leave catalog_id blank and fill in all five of the following:

  • product_name: What to call the product. Example: Custom Gold Bar, Heraeus 100g, Royal Mint Britannia 1oz (2023).

  • metal: One of GOLD, SILVER, PLATINUM, PALLADIUM. Lowercase is accepted, but uppercase is the convention the template uses and the form the value will show as inside the app.

  • form: One of COIN, BAR, ROUND, JUNK. Lowercase is accepted here too; uppercase is the canonical form.

  • weight_oz: The per-unit weight in troy ounces, as a decimal. 1 for a one-ounce piece, 0.5 for a half-ounce, 3.2151 for 100g.

  • purity: A decimal strictly greater than 0 and strictly less than 1. 0.9999 for fine gold, 0.999 for fine silver, 0.917 for pre-1965 US 90% silver coins. Entering purity as a percentage (e.g. 99.99) flags the row.

What the import does with these fields depends on whether you've used the same product_name before:

  • If a custom item with that exact name already exists in your account, the row is matched to that existing custom item — no duplicate is created.

  • If no match is found, the import creates a new custom item from the fields on the row and uses it for the transaction. The new product shows up on the My Custom Items tab of the Catalog afterward.

Don't mix the two paths on the same row

If catalog_id is filled in and the custom fields are filled in, the import uses catalog_id and ignores the custom fields. The safest pattern is to leave the unused side blank. The template's sample rows show both shapes back-to-back so you can copy whichever fits.

quantity (required)

How many units of this product you bought on this row, as a positive whole number. The import creates one inventory item per unit — quantity: 10 becomes ten separate item records, exactly like the Record Purchase form. Each row produces one BUY transaction in your ledger, regardless of how big the quantity is.

Negative values flag the row. Bulk Import handles BUY transactions only — there's no "negative quantity" path for recording a sale. Use the Record Sale form for those; see How to record a sale.

If you bought ten of the same product across two separate orders on different dates, that's two rows in your CSV with quantity 10 each — not one row with quantity 20. The date and the dealer want to be accurate per transaction.

A note on currency

All four monetary columns — price_per_unit, premium_per_unit, spot_at_time, and shipping_cost — are interpreted as USD. Bulk Import does not read your display-currency preference when parsing the file.

If you paid in another currency, convert the amounts to USD before you enter them in the CSV — ideally at the FX rate that applied on the purchase date so the recorded values reflect what the transaction actually cost in dollars at the time.

Once a row is imported, the rest of the app behaves normally: stored values are in USD and converted back to your display currency at render time, exactly like a transaction entered manually.

The Bulk Import page surfaces this in the How it works panel as well, so a quick glance there before you start is a good idea.

price_per_unit (required)

The amount you paid per coin or bar, in USD. This is the only price column that has to be filled in on every row.

Don't put the all-in total for the row here — price_per_unit is per item. If you bought 5 silver Eagles for $190 total, the right value is 38, not 190.

For a worked walkthrough of how price_per_unit, premium_per_unit, and spot_at_time interact, see Bulk import overview — the section on how the three columns are derived from each other is the canonical version of that math.

premium_per_unit (optional)

The per-unit premium you paid over spot, in USD. Used only when spot_at_time is left blank on the same row, in which case the import works backwards from price_per_unit and premium_per_unit to figure out the spot that applied at the time. If spot_at_time is filled in, spot_at_time wins and any value here is ignored.

spot_at_time (optional)

The spot price per troy ounce that applied at the moment of the purchase, in USD per troy ounce.

For historical purchases, fill it in from your invoice or a public chart for that date — that's the whole point of the column. If you leave both spot_at_time and premium_per_unit blank, the import falls back to the current live spot for the metal. That's fine for a purchase you made today, but rarely right for one from years ago.

shipping_cost (optional)

The total shipping, insurance, or handling for the whole row, in USD. The import stores it on the transaction separately from the per-unit price, and the rest of the app allocates it across the row's individual items in proportion to each one's pre-shipping cost.

Leave it blank or set it to 0 for rows where shipping was already rolled into the per-unit price, or for in-person purchases where there wasn't any shipping. For the longer version, see Adding shipping and handling costs.

dealer (optional)

Free-text dealer name. Stored exactly as typed on the transaction — no normalization, no auto-matching, no auto-creation of a saved dealer entry. Two implications worth knowing:

  • Be consistent with spelling and casing across your rows. APMEX and Apmex are two separate dealer names as far as the app is concerned, and they won't roll up together on the Transactions History page or in filters. Pick a spelling and stick to it.

  • This column is independent of your saved dealer list. Adding a dealer here doesn't add it to your saved list, and a dealer in your saved list isn't required for this column to accept the name. They're two different things that happen to share a word.

Leave the column blank for private sales or any row where you'd rather not record a source.

notes (optional)

Free-text notes attached to the transaction. Anything you want to remember about the purchase: a confirmation number, the condition or grade, who gave it to you, what the market was doing. Notes show up in the expanded view of the transaction on the History page and in CSV exports.

A note on portfolio targeting

Bulk Import doesn't have a portfolio column. Every row in your CSV is imported into whichever portfolio is currently selected in the portfolio switcher at the top of the app.

  • If a specific portfolio is selected, a blue notice at the bottom of the How it works panel names it. Every row in the upload lands there.

  • If "All Portfolios" is selected, the import falls back to your first portfolio and an amber warning appears in the same place naming the destination. Switch to the specific portfolio you want before you upload, and the warning becomes a confirmation.

The notice is always on screen, so you'll see it before you upload rather than discovering the destination after the fact. If you keep your stack split across multiple portfolios — personal, household, trust — change the selector before you start each import.

Where to go next

Did this answer your question?