What actually survives a Stocky export — I went through the API docs field by field

Two things to add to thoopring’s first point, because the shape of that failure is worth naming exactly.

The usual way a paged export decides it has finished is that a page comes back shorter than the page size. That is a fine rule right up until a page comes back short for some other reason, and then the walk stops early, the file gets written, and nothing anywhere reports a problem. That is the “opens fine, imports fine, missing a chunk in the middle” case, and comparing the count Stocky shows against the row count afterwards is the check that catches it. The trailer idea is a good one for the same reason.

There is a second version that a row count on its own does not always catch. If the pagination direction is wrong for a resource, the same page can come back over and over, so the walk either hangs or quietly covers the same ground twice. Keeping the set of ids already collected and stopping the moment a page adds nothing new turns that into a named warning instead. The general point behind both: the quiet path and the successful path look identical from the outside, so the export needs to say what it thinks it got, out loud, every time.

On supplier_cost_price and received_at, agreed, and worth being blunt about. Both sit at the line item level rather than the purchase order level. Any export shape that hands you one row per PO has already thrown the receipt history away, whatever its field list says, and that is the history you need if you ever want real supplier lead times later. Keeping line items as their own file instead of flattening them is the whole difference.

Roberto_Teng, on your diff question: not from us. We have never taken one export into two replacement apps and compared what actually landed on the other side. If you do write up the CSV shape that imports cleanly, that is the missing half of this thread.

Disclosure: we build a Stocky exporter, so discount this accordingly. Being straight about its limits, it is tested against a mock built from Stocky’s published API docs and it has never run against a live store, because Stocky cannot be installed fresh any more. So treat the field names above as what the documentation says, not as something confirmed on a real account.

And thoopring’s last point is the one with a clock on it. The date applies to the API, not just the export screen.

Following up on the CSV shape I offered upthread, with a correction to my own offer first.

I said I’d write up what imports cleanly. Going back through our importer to do it, I have to be straight: it has never seen a real Stocky export. Not one. It was built against header names we guessed Stocky emits, it has a single unit test with synthetic data, and there are zero imports in production. So I can tell you exactly what our side expects — but not what Stocky actually gives you, and this thread has been careful enough about that distinction that I won’t blur it.

What is real, and probably generalises to anyone writing one of these:

Cost parsing is where money silently disappears. Ours does parseFloat(value) || null, which looks fine until real data arrives:

  • 12.50 → 12.50 ✓
  • 0 → null — a genuine zero cost imports as blank
  • $12.50 → null — any currency symbol drops the cost entirely
  • 1,250.00 → 1 — a thousands separator imports as one dollar

The last one is the one that worries me. It isn’t empty, it’s confidently wrong, and it survives any “did the costs come through” eyeball check.

Received quantities don’t carry. We read a Received column, sum it per PO, and use it only to set the PO status. The number never reaches the line. So a half-received Stocky PO imports looking untouched, and you’d receive the whole thing a second time. If you’re migrating mid-delivery, check this in whichever tool you land on — it’s a quiet way to double your stock.

Dates and locations get dropped. We accept Date, Order Date, Created At, Expected Arrival, Location and Notes in the mapper and then use none of them. Expected arrival is never populated by an import. Which means the received_at discussion earlier in this thread is one I can’t honestly contribute to — we aren’t reading the dates we accept.

Both the cost parsing and the received quantities are being fixed this week.

For what it’s worth, the header row we currently expect is PO Number, Supplier, Product Title, Variant, SKU, Barcode, Quantity, Received, Cost — line-level, one row per product, grouped into POs by PO number. One trap: if the PO number column isn’t mapped, every row becomes its own single-line PO.

The actual ask: does anyone here have a real Stocky export they’d share the header row from? Not the data — just the headers. Our alias list is guesswork and I’d rather correct it against something real while Stocky is still alive to export from. Two days left on that.

Great deep-dive on Stocky’s export limitations. > The cost truncation issue (1,250.00 converting to 1) usually happens when the parser strips non-numeric characters after applying parseFloat() without first stripping comma thousands separators.

A few workarounds depending on your pipeline:

  • Data Normalization: Run a pre-processing script (Python/Pandas or Node) to strip currency symbols ($, ) and remove commas before passing the payload to the import endpoint.

  • Partial PO State Handling: Ensure line items track quantity_received explicitly rather than relying on total delta states, preventing inventory double-counts during syncs.

We ran into these exact CSV edge cases while building our inventory platform (SyncPlus), specifically designing the ingestion engine to sanitize localized currency formats and preserve line-item PO histories out of the box. Happy to share our schema handling if helpful!

Roberto_Teng, taking your ask straight first: no, we don’t have a real Stocky export either. I said that last time and I’m not going to blur it now that it would be convenient. Ours is built against the published v2 docs and a mock, and it has never touched a live store.

What I can give you is the other side of it, and I think it’s the half your alias list actually needs, because there are two different artifacts floating around this thread and they don’t carry the same fields. Your header row (PO Number, Supplier, Product Title, Variant, SKU, Barcode, Quantity, Received, Cost) reads like the CSV the export screen emits. The API returns something else. I re-read the v2 docs pages this morning to check every field below rather than quoting my own notes at you, and they’re still returning 200 if anyone else wants to look.

A line item in the API comes back as: id, sku, inventory_item_id, product_title, variant_title, asin, quantity, status, retail_price, cost_price, supplier_cost_price, account_code, tax_type_id, accounting_tax_type, received_at, updated_at.

Four things in that which bear on your list:

No barcode. It isn’t in the payload, so that column can never populate from an API-sourced file no matter how good the aliases get. There is an asin field, which I haven’t seen on anyone’s alias list.

Cost is three fields. retail_price, cost_price and supplier_cost_price all exist and mean different things. supplier_cost_price is the per delivery price actually paid, which is the one carrying the history somebody will want later for lead times and margin. A single Cost column has to pick one, and picking the wrong one is a silent wrong answer rather than a blank, which is the failure mode you already flagged as the one that worries you.

Received isn’t a quantity. There’s no received-quantity field anywhere in the payload. What you get is received_at, a timestamp on each line, plus a per line status whose sample value is the string “not delivered”. So the “we sum a Received column per PO” behaviour isn’t only a mapping bug, it’s a shape mismatch: on an API sourced file there’s nothing to sum, and the receipt information is a date sitting on the line already. That probably makes your fix smaller than you were expecting, though it does change what half received looks like.

Watch the dates on line items specifically, because the format is not the same as everywhere else. In the docs’ own example the purchase order header carries created_at as 2021-02-09T00:23:25.000Z, and the line item nested inside that same order carries updated_at as 09/02/2021 00:23. Same moment, two formats, and the line-item one is day first. If your parser defaults to month first, as most do, every line item silently moves by up to eleven months and nothing errors. Worth a look given you said you accept date columns and then don’t read them, since that’s the bug you’d get for free the moment you start.

One more from the same page: retail_price comes back as a quoted string while the order-level adjustments and shipping come back as bare numbers, so the types are mixed within a single response.

On 1,250.00 becoming 1, since #28 offered a mechanism for it: it isn’t strip-then-parse. parseFloat reads left to right and stops at the first character that can’t continue a number, so it sees the 1, hits the comma, and returns 1 having never looked at the rest. Nothing is stripped. That only matters because the fix has an order to it, strip separators before you parse and never after, and after is the version that looks like it works. Your other two are the same shape from the other operator: parseFloat(“$12.50”) is NaN and NaN || null is null, parseFloat(“0”) is 0 and 0 || null is null, so a genuine zero and a currency symbol land in the same place for different reasons. Swapping || for ?? fixes the zero on its own and leaves the rest of your logic alone.

Last thing, and it’s the one I’d put a column in the mapper for: if the file came through the API, the numbers arrive as JSON with no separators and no currency symbols at all, so the comma bug can’t fire on it. Which source a merchant’s file came from decides which of your four bugs are even reachable, and right now you can’t tell from the file.

Disclosure, same as before: we build a Stocky exporter, so discount all of this accordingly. Everything above is transcribed from Stocky’s own v2 documentation rather than from a live response, for the reason at the top.

This is the most useful thing anyone has given me on this, and the two-artifact point reorganises the problem. Our alias list was one list because I assumed one file. It’s two.

On telling the sources apart — I think the file does tell you, just not from the columns we’re mapping. An API-sourced file carries inventory_item_id, asin, account_code, tax_type_id. A CSV one carries Barcode, which the API doesn’t have at all. So the provenance column you’d put in the mapper can probably be inferred rather than asked: asin or inventory_item_id present means API, Barcode present means export screen. And since provenance decides which bugs can even fire, it isn’t a label — it’s a switch.

One correction on ||??, because I nearly shipped it as written. parseFloat("") is NaN, and NaN ?? null is NaN — ?? only catches null and undefined. So the swap fixes the genuine zero and quietly starts storing NaN for empty cells, which is a worse failure than the one it replaces. It needs the operator swap and an explicit NaN check. Trading one silent wrong value for another would have been a stupid way to fix a bug about silent wrong values.

The date one I’d have shipped without noticing — we accept the columns and don’t read them, so “the bug you’d get for free the moment you start” is exactly right. Partial mitigation for anyone else reading: if any day-position value in a file exceeds 12 the format is unambiguous and you can sniff it. If every date happens to fall in the first twelve days of its month, you can’t, and you have to ask.

supplier_cost_price as the per-delivery price actually paid is the one I’d have got wrong. A single Cost column picking cost_price looks correct and quietly loses the history someone wants later — same failure shape as the comma.

We’re both working from docs rather than a live store, and that’s the binding constraint on this thread now, rather than anything either of us can reason our way out of.

You’re right, and I want to correct it properly rather than just concede it, because I’m the one who put that line in front of you.

What I wrote was that swapping || for ?? “fixes the zero on its own and leaves the rest of your logic alone”. First half true, second half wrong. ?? only catches null and undefined, and parseFloat hands back NaN for everything it can’t read, so the swap moves your empty cells and your $12.50 cells from null to NaN. I ran it before writing this, having already told you one wrong thing:

value        parseFloat   || null   ?? null
"12.50"      12.5         12.5      12.5
"0"          0            null      0        <- what the swap fixes
"$12.50"     NaN          null      NaN      <- what it breaks
""           NaN          null      NaN      <- what it breaks
"1,250.00"   1            1         1        <- unchanged either way

And NaN is a bad thing to be storing quietly. JSON.stringify turns it into null anyway, a CSV writer prints the literal text NaN, and NaN === NaN is false, so any later “is this value the same as it was” check answers no forever. You were right to stop.

The version I should have given you, since strip-before-parse was the actual point:

function parseCost(v) {
  if (v === null || v === undefined) return null;
  const s = String(v).replace(/[^0-9.\-]/g, "");
  if (s === "" || s === "-" || s === ".") return null;
  const n = Number(s);
  return Number.isFinite(n) ? n : null;
}

Two notes on why it’s shaped like that. Number rather than parseFloat, because parseFloat("12abc") is 12 while Number("12abc") is NaN, and that left to right truncation is the same family as the comma bug. And the empty check is not tidiness: Number("") is 0, so dropping that line turns every blank cell into a genuine zero cost, which is the bug you started with arriving from the opposite direction.

Two things it still gets wrong, said out loud so nobody inherits them from me:

  • "1.250,00" comes out as 1.25. A decimal comma locale has to be decided before you parse and you cannot sniff it from one value.
  • "(12.50)" comes out positive. Parenthesised negatives lose their sign.

Both are only reachable on a CSV sourced file, which is your provenance switch already earning its keep.

On provenance, agreed, and inferring beats asking. One caution: absence is much weaker evidence than presence. Somebody who trimmed an API export down to the columns they cared about can have dropped asin and inventory_item_id and still be handing you an API file. So treat a present marker as decisive, a missing one as unknown, and let unknown take the careful path.

On the binding constraint, you’re right, and it isn’t going to lift now. What we did about it instead of reasoning around it: we turned what this thread has established into a test fixture, and I’d rather hand it over than describe it.

It’s one Python file, standard library only. It runs as a mock of Stocky’s v2 API on localhost behind the same two auth headers, or it dumps static JSON. Thirty purchase orders, five suppliers, and every documented trap reproduced on purpose: the backwards pagination on purchase_orders, drafts missing from the unfiltered list, all eight status values, the day first line item dates against ISO headers, string prices against numeric adjustments, three cost fields set to three different values on one line, a genuine zero cost, a half received order with nothing to sum, a supplier_id that is a string joining to an id that is a number, unicode in the SKUs, an empty items array. Each one is listed in a TRAPS file with the docs page it came from, so you can tell the mock being awkward from Stocky being awkward.

It is built from the published docs, same as everything else I’ve said here, and its own header says so. What it proves is that an importer survives the spec. It cannot prove the live API matched its spec, and nothing can now.

If you want it, say so here or write to aislekit at gmail and I’ll send it over. No charge, no strings, and if it turns up a bug in the fixture I’d like to hear that too. Said once so nobody is surprised later: if you’d rather we ran your importer against it and wrote up exactly what broke and why, that is work we would charge for and we can take it off thread. The fixture is free either way.

Disclosure as before: we build a Stocky exporter.

(post deleted by author)

Solid field-by-field map — thank you for putting the API vs CSV split in one place.

Agreeing the hard bits that still matter now that Stocky UI is dark:

No automated path (manual or lose it): supplier notes, par / min-max reorder levels, and the supplier list itself. Shopify Help Center is blunt: “Suppliers can’t be exported from Stocky.” Contact/address may have lived in the old API; the free-text notes and par settings did not ride out on a clean bulk file. If you haven’t already copied those into a doc or Sheet, do it while read-only still works — Help Center says export access for at least 90 days after 31 Aug 2026 (confirm the window in your admin; don’t trust a blog countdown).

CSV still useful: completed PO line history and stocktake-style exports are the practical rebuild inputs for most stores. What the standard CSV does not cleanly give you is item-level received_at (actual arrival vs PO create date). That was the API-side urgency before Aug 31. I am not saying any Sheet or importer can resurrect API-only received_at after the API closed — if you didn’t pull it then, treat lead-time math from receive dates as gone unless you logged it elsewhere.
If you want another inventory app: use one. Stockroom - Purchase Orders (MyWorks) is listed Free on the App Store (45 reviews, 5.0 as of early Sep): migrate-from-Stocky pitch, unlimited POs/suppliers, email POs as PDFs, partial receive. Native Shopify POs also exist — vendor download is still Export PDF per Help Center. I’m not affiliated with Stockroom or the other tools people have disclosed in this thread (including the OP’s extension). Different jobs; don’t skip a good app because someone is selling a spreadsheet.
If your gap is specifically “owned supplier register + vendor CSV + dated cost notes” without another OAuth app: I built a Google Sheet for that loop — retype suppliers once into Drive, paste an Orders export for reorder qty, download a vendor PO as CSV + printable, keep a receiving/cost snapshot log. It does not sync with Shopify and it does not recover API-only received_at. Weekend flash $19 through 6 Sep (then $39). 14-day Gumroad refund. I made it — stating that so this isn’t a drive-by.