DEV Community

Cover image for Why Adobe Rejects Your Metadata CSV Header Instantly And How To Fix It

Why Adobe Rejects Your Metadata CSV Header Instantly And How To Fix It

I still have the rejection email from Adobe Stock saved in a folder called "failures". My first batch was 40 vector files, weeks of work, and I uploaded them with a CSV I built by hand in a spreadsheet. Every single row came back rejected before any human ever looked at the art. The reason was the header row. Not the keywords, not the titles, not the files. The header.

Here is what I learned fixing that, and why it matters more than almost anything else in the pipeline.

Adobe checks the header before anything else

Adobe's uploader reads a strict 5-column format. The header row must be exactly these five columns, in this order:

Filename,Title,Keywords,Category,Releases
Enter fullscreen mode Exit fullscreen mode

My hand-made CSV had a "Description" column because it felt natural. Adobe does not care what feels natural. A wrong column name or a wrong column count means the file is rejected instantly, at the parse stage, before your images are even considered. There is no partial credit. There is no "close enough". Either the header matches what their parser expects or the whole upload fails.

The same pattern holds on the other channels I sell to. Vecteezy wants a 4-column format, and Dreamstime wants 15 columns. Three platforms, three different headers, zero tolerance. If you hand-build these spreadsheets you will eventually get one wrong, and the failure mode is confusing because the error message points at your data, not at the real problem.

The fix is boring and total

I stopped writing CSVs by hand. make_metadata.py in my pipeline takes a folder of files and writes the header each platform demands, and nothing else:

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

Three commands, three correct headers, no chance of a typo. The script uses only the Python standard library, so there is nothing to install. By default it title-cases the filename into a title, which is fine for a first pass but you should replace those with real titles and keywords before you submit. Pass --titles titles.json and it uses your prepared titles instead.

The important design decision was that the script writes the header the platform demands, not a header I invented. Every column is copied from the platform's own spec, checked against review behavior, not guessed.

The second instant-rejection trap: double-posting

Once the header was fixed I hit the next failure. I re-ran a batch to fix one bad file and accidentally re-uploaded the whole set. Uploading the same asset twice is one of the fastest ways to get an account flagged, and batch tools make it easy to do by accident.

So the pipeline got a ledger. upload_tracker.py remembers what has already been posted:

python upload_tracker.py pending items.txt   # what is NOT done yet
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
Enter fullscreen mode Exit fullscreen mode

A re-run now skips anything already marked done. It is a plain text file and a few dozen lines of standard library Python, and it has saved me from at least two flagging-level mistakes since.

Why I build metadata in code at all

Some people tell me spreadsheets are fine. They are, at 10 files. At 100 files per batch, across three platforms, hand-built CSVs stop scaling and start failing. My parent project is around 19,000 lines of pipeline code and has packaged 39 finished products, 856 MB of output. None of that would have been possible if every upload required me to open a spreadsheet and pray.

The whole thing also runs on a 1 vCPU, 2 GB RAM server with no GPU. Metadata generation costs nothing computationally, so it fits anywhere. The expensive part is image work, and there the habits are: one process at a time, gc.collect() between assets, measure peak RSS with resource.getrusage, and render at the resolution you actually need. Inspecting a preview at 60 dpi instead of 300 is the difference between fitting in 2 GB and swapping to death.

The checklist I now run before every upload

  1. Header matches the platform exactly. Adobe 5 columns, Vecteezy 4, Dreamstime 15.
  2. Titles and keywords are real, not filename title-case defaults.
  3. upload_tracker.py status confirms nothing in this batch was already posted.
  4. Files are in output/, metadata CSV generated fresh from that folder, never edited by hand afterward.

That checklist came from three rejections that were all my fault. The 300+ pages of research behind the platform rules in my notes would fill several posts like this one, but the metadata header is the one that bites everyone first, because it fails before your work is even seen.

The tool I built from this

Everything above is packaged in the Pipeline Starter Kit: the four working scripts (make_metadata.py, upload_tracker.py, md2pdf.py, pack_product.py), the folder layout, and the tiny-server notes. These are the same scripts that produced those 39 products, not a rewritten tutorial version. If you are assembling your own pipeline, it saves you the rejection emails.

See the kit


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

Top comments (0)