DEV Community

Cover image for Why Wrong CSV Headers Get Rejected Instantly, And The Script That Writes Them Correctly

Why Wrong CSV Headers Get Rejected Instantly, And The Script That Writes Them Correctly

I lost four hours of my life to a CSV header. It was a Tuesday. I had 62 finished JPGs ready for Adobe Stock, a metadata CSV I'd built by hand in a spreadsheet, and a review queue I assumed would take a few days. Every file came back rejected within minutes. Not "quality concerns", not "keyword issues". The rejection reason was the same on all 62: invalid file format. The problem wasn't the images. It was the header row. I had written Filename, Title, Keywords, Category because that's what made sense to me. Adobe's bulk upload expects a specific five-column layout, and their parser checks the header row before it looks at anything else. Wrong header, instant reject, no feedback about which column was wrong. That failure is why make_metadata.py exists, and why I stopped hand-building CSVs entirely.

The header is the first thing their parser reads

This is the part nobody tells you. Stock platforms don't "review" your CSV in any human sense at the upload stage. A script reads it. That script validates the structure first: exact column count, exact column order, exact header names. If the structure fails, the file never gets to a reviewer. Adobe's bulk upload template is five columns. Vecteezy's is four. Dreamstime's is fifteen. Those are not suggestions, they are the schema their ingestion code expects, and each platform rejects anything that deviates. The nasty part is that the error messages don't say "your header is wrong". They say the file couldn't be processed, and you burn a retry cycle guessing. I did that guessing three times before I opened the actual template file and compared it character by character against mine. The difference was column order and one header name I'd abbreviated.

Why the three platforms don't agree

You might assume a CSV is a CSV. It isn't. Adobe wants five columns covering filename, title, keywords, category, and releases. Vecteezy wants four. Dreamstime wants fifteen, because their format carries description fields, extra keyword slots, and metadata their reviewer workflow depends on. If you maintain one master spreadsheet and export variations of it, you will eventually get one of the three wrong, because you're transforming by hand under deadline pressure. The fix that actually held up for me was to stop writing CSVs by hand and generate them from one source of truth. Every asset lives in a folder with a filename. A script walks the folder, applies the platform's exact header, fills the columns, and writes the file. The header is not typed by a human ever again, so it cannot be wrong.

What the generator actually does

make_metadata.py takes a folder of files and a platform flag, and writes the CSV with the exact header that platform's parser expects. Adobe gets five columns. Vecteezy gets four. Dreamstime gets fifteen. Nothing else. By default it title-cases the filename and uses that as the title, which is fine for a first pass but I always replace it. There's a --titles titles.json flag where you supply real titles and keywords per file, and that's what I use before anything gets submitted. Filenames as titles read like filenames, and reviewers notice. The script itself is standard library only. No pandas, no CSV library with opinions, no install step. That matters because my whole pipeline runs on a 1 vCPU, 2 GB RAM server with no GPU, and every dependency you add is memory you don't have. Two scripts in the kit, the metadata builder and the upload tracker, need nothing but Python.

The second way to get flagged: running the batch twice

Generating correct CSVs solved one failure mode and created the risk of another. Once uploads are scripted, it becomes trivially easy to run the same batch twice, especially after a partial failure where you're not sure which files went through. Double-posting is the fastest way I know of to get an account flagged, and on a small account you don't get the benefit of the doubt. So the second half of the fix is upload_tracker.py, a plain ledger that remembers what's been marked done. pending shows what hasn't been posted, done marks an item, status gives the overview. A re-run of the batch skips anything already in the ledger. It's not clever. It's a text file and discipline, and it has prevented every duplicate I would otherwise have created since.

The boring lesson

Every rejection I've had from a stock platform traces back to one of two causes: structure the platform's parser didn't accept, or submitting something twice. Neither is an art problem. Both are process problems, and both are solved by making the correct thing the automatic thing. Hand-typed headers and memory-based upload tracking scale to about twenty files before they fail. I have 39 finished products and around 19,000 lines of pipeline code behind this workflow now, all of it validated on that same 2 GB box, and I haven't had a header rejection since the day I stopped typing headers.

I packaged the working versions of these scripts, the exact Adobe, Vecteezy and Dreamstime header handling plus the upload ledger, as the compliance metadata kit. It's the tool I built out of that Tuesday, not a course about it, and the metadata scripts run with zero installs.

Get compliance metadata kit


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

Top comments (0)