I lost three hours of batch uploads in one morning because of a header row. Not the images, not the keywords, the literal first line of the CSV. I had 40 files ready for Adobe Stock, generated the metadata with my own script, hit submit, and got the rejection screen. The file contents were fine. The column order was not. That morning I wrote down every header format the three platforms I sell on actually expect, and I have not had a metadata rejection since. Here is what I learned, in case you are about to make the same mistake.
Adobe wants five columns, in that order
Adobe Stock's contributor CSV is a 5-column format. The header row is fixed, and the reviewer pipeline checks it before it ever looks at your art. Get the column count or order wrong and the upload fails at validation, instantly, with no partial credit for a mostly-right file.
What saved me was stopping the guesswork. My script, make_metadata.py, now writes the exact 5-column Adobe header and nothing else. No extra columns "for future use", no reordered fields because I thought one order read better. The platform does not care what reads better. It cares about the template.
One more Adobe note: filenames matter as much as the CSV. The script title-cases the filename into the title field by default, which is fine for a first pass, but you should replace it with a real title and real keywords before you submit. I pass a titles.json file with hand-written titles for anything I actually care about ranking.
Vecteezy is a different beast at four columns
Vecteezy uses a 4-column metadata format. If you copy your Adobe CSV and just delete a column, you will get it wrong, because the fields do not map one to one. This is the mistake I made on my second batch. I assumed the platforms had converged on something similar. They had not.
The fix was boring: one code path per platform, each writing its own header from scratch, no shared template with conditionals. It is slightly more code and it means a format change on one platform never breaks the other two. When Vecteezy tweaked their requirements, I changed one function in one file and re-ran the batch.
Dreamstime is the long one: 15 columns
Dreamstime is the widest of the three at 15 columns. This is where I lost the most time originally, because a 15-column CSV gives you fifteen places to be wrong and the rejection message does not tell you which one. My first Dreamstime attempt had a mismatch in a middle column that took me an embarrassing amount of trial and error to find.
The lesson: never hand-build a 15-column row. Generate it. My script builds the full Dreamstime row from the same folder of files that produces the Adobe and Vecteezy CSVs, so one asset folder produces three correct metadata files in three commands:
python make_metadata.py ./my-assets --platform adobe --out adobe.csv
python make_metadata.py ./my-assets --platform vecteezy --out vecteezy.csv
python make_metadata.py ./my-assets --platform dreamstime --out dreamstime.csv
Same source files, three headers, zero chance of a column-count typo at 2am.
The rejection that actually hurts is the double-post
Wrong headers get rejected loudly, which is annoying but safe. The quiet killer is running your upload batch twice. I did this once during a re-run after a partial failure, and double-posting is one of the fastest ways to get an account flagged. Rejections are a speed bump. A flagged account is a business problem.
So now every batch runs through a ledger. upload_tracker.py is a plain standard-library script that records what has been posted:
python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
A re-run checks the ledger first and skips anything marked done. It is about forty lines of Python and it has prevented every repeat of my one flagged-batch scare. If you take one thing from this post, take this one. Metadata rejections cost you minutes. Double-posts can cost you the account.
All of this ran on hardware you already beat
None of this needs a real machine. The whole pipeline, metadata generation, tracking, PDF rendering, packaging, ran on 1 vCPU and 2 GB RAM with no GPU. The habits that made it work: one process at a time, gc.collect() between assets, measuring peak RSS with ru_maxrss instead of guessing, and rendering at the resolution you actually need (inspecting previews at 60 dpi instead of 300). If your laptop has more than 2 GB of RAM, you have more headroom than I did.
For context on scale: this setup packaged 39 finished products, about 856 MB of output, on top of roughly 19,000 lines of pipeline code and 300+ pages of research behind the rules the scripts enforce. The metadata scripts themselves use only the Python standard library. No installs, no cloud account, no framework.
Where to get the exact scripts
Everything above is in the Pipeline Starter Kit, and the metadata work is the part I would have paid for back when I was staring at that first rejection screen. The kit is four standalone Python scripts: make_metadata.py (the Adobe 5-column, Vecteezy 4-column, and Dreamstime 15-column headers, exactly as the platforms demand), upload_tracker.py (the double-post ledger), plus md2pdf.py for turning Markdown into styled PDFs and pack_product.py for bundling zips with a 1280×720 cover and 600×600 thumbnail. It plugs into the vector prompt pack workflow I use to go from prompt to finished, submittable product.
If you want the same headers I use, they are here:
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)