DEV Community

Cover image for Why One Wrong Column In Your Stock Upload CSV Gets The Whole Batch Rejected

Why One Wrong Column In Your Stock Upload CSV Gets The Whole Batch Rejected

Last March I lost a full weekend to a single header row. I had 120 finished assets sitting in output/, metadata built, uploads queued. I submitted the Adobe batch and got rejections back within the hour. The cause was one column. I had written Keywords where the template demanded Keywords; style parsing, and Adobe's validator does not care that your actual data is fine. A wrong header means the whole CSV is unreadable to their importer, so all 120 rows came back rejected, not just the broken one. That is the part that stings: one mistake costs you the batch, not the row.

Here is what I learned fixing it, and why the metadata layer is now the most defensive code in my whole pipeline.

The headers are not negotiable, and they are not the same

Every stock platform has its own upload CSV format, and they do not converge. The three I sell to:

  • Adobe Stock wants 5 columns: Filename, Title, Keywords, Category, Releases.
  • Vecteezy wants 4 columns: Filename, Title, Keywords, Releases.
  • Dreamstime wants 15 columns. Fifteen. It includes things the other two never ask for, and every one of them has to be in the right position.

The failure mode is always the same. You build one master spreadsheet, then hand-edit a copy for each platform, and one of those copies drifts. A renamed column, a missing one, an extra trailing comma. The validator does not give you a helpful diff. It rejects the file, and you get to find the problem by eye across 15 columns and 120 rows.

So I stopped hand-editing. make_metadata.py in my pipeline takes a folder of assets and writes the exact header each platform demands, and nothing else. One command per platform:

python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg
Enter fullscreen mode Exit fullscreen mode

The header is hardcoded, not generated. That is deliberate. A template string that never changes cannot drift.

The second silent killer: running the batch twice

This one is worse than a rejected CSV, because it can get your account flagged rather than just your files bounced. If your upload script crashes halfway through a batch of 120, your instinct is to re-run it. If it does not remember what it already posted, you double-post 60 files. Platforms treat duplicate submissions as spam behavior, and enough of it puts a flag on your account that you will never see but will definitely feel in review times.

The fix is boring and essential: a ledger. upload_tracker.py is a tiny script with three commands:

python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
Enter fullscreen mode Exit fullscreen mode

Every successful upload marks the file done before the next one starts. A re-run skips anything already in the ledger. It uses the standard library alone, no install. It has prevented exactly one double-post incident, and one is enough to justify it forever.

Titles and keywords: automate the structure, not the judgment

The script will title-case your filename and use it as the title if you do not supply one. That is fine for a first pass, and I want to be honest about its ceiling: filenames like geometric-pattern-004.jpg make terrible search titles, and platforms rank on them. Before anything is submitted, I pass --titles titles.json with real titles and keywords per asset. The script handles the CSV structure, I handle the words. Trying to automate keyword judgment with templates is how you end up with 120 assets that all share the same 8 generic tags and rank for nothing.

What this looks like on a real machine

None of this needs infrastructure. The whole pipeline, metadata generation, upload tracking, PDF rendering, product packaging, ran on a 1 vCPU, 2 GB RAM server with no GPU. The metadata and tracking scripts use only the Python standard library. The only third-party packages anywhere are Pillow for cover images and reportlab for PDFs. Two pip install entries for an entire product pipeline.

The habits that make it survive a box that small: one process at a time, gc.collect() after each asset, measure peak RSS with ru_maxrss instead of guessing, and render at the resolution you actually need. Inspecting a preview at 60 dpi instead of 300 is the difference between fitting in RAM and swapping.

The folder layout that saved my sanity

The last lesson was organizational. Once you have 39 products, an unstructured folder becomes a junk drawer and you lose track of what is submitted, what is packaged, and what is raw. The structure I settled on:

project/
  assets/      raw output
  output/      ready-to-submit files + metadata.csv
  packs/       finished product zips + covers
  research/    notes and sources
  scripts/     the tools
Enter fullscreen mode Exit fullscreen mode

One warning from experience: never delete research/ to clean up. I did that once. Those notes are the raw material for the next product, and I have not repeated the mistake.

Where the tools live

Everything in this post is real code I use weekly, and I packaged it as the Pipeline Starter Kit: four standalone scripts (make_metadata.py, upload_tracker.py, md2pdf.py, pack_product.py), built from a parent project of about 19,000 lines that packaged 39 products and 856 MB of output, with 300+ pages of platform research behind the rules the scripts enforce. It is the tool I wish I had the day that CSV came back rejected.

If you are also making art from prompts and want the content side, the vector prompt pack is the prompt engine that fed this pipeline in the first place.

Get vector prompt pack


Building digital products? Free guides & kits for digital product sellers.

Top comments (0)