Last March I uploaded 40 files to Adobe Stock in one batch. The CSV had a header I'd written from memory. Adobe rejected the whole file in about two minutes. Not the images, the CSV itself, so nothing even got into review. I fixed the header, resubmitted, and got a different rejection: two of my keyword columns were swapped. That was the moment I stopped hand-editing spreadsheets and wrote a script instead. This post is what that script does, and why the headers matter more than you think.
The headers are contracts, not suggestions
Each marketplace has its own CSV format, and each one validates the header row before it looks at a single image.
Adobe Stock wants 5 columns: Filename, Title, Keywords, Category, Releases. Get the column count or order wrong and the upload fails before review. Category is a number from their fixed list, so my script writes a placeholder category value that you replace, because guessing a category number by hand across 40 rows is how mistakes happen.
Vecteezy wants 4 columns. Dreamstime wants 15. Same folder of files, three different outputs. My make_metadata.py reads a folder, walks the files matching your extension, and writes the exact header each platform demands and nothing else. It takes maybe a second for a few hundred rows because it's standard library Python only. No pandas, no install, no cloud account.
The honest caveat: without a titles file, the script title-cases your filename and uses that as the title. That's fine for a first pass. Replace it before you submit, because a title like "Blue Geometric Pattern 04" does not help discovery on any of the three platforms.
Double-posting is the fastest way to get flagged
This one cost me a scare early on. I ran a batch upload, the connection dropped halfway, and I couldn't remember exactly which files had gone through. Re-uploading a file that already passed review is the kind of duplicate content that gets accounts flagged on every major marketplace.
So the second script is dumb on purpose. upload_tracker.py is a ledger. Three commands:
python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
Before any batch, I run pending and only feed the unposted files into the uploader. It uses the standard library alone. It has no opinions about storage, no database, just a text ledger I can read and diff. Boring tools you can inspect beat clever tools you can't.
One script, three platforms, zero drift
The reason I merged all three platforms into one script instead of three separate files is drift. The first version had separate scripts, and within two weeks the Vecteezy one had a keyword-limit fix the Adobe one didn't. That's how bugs are born.
One script, one --platform flag:
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
Every platform rule lives in one place. When a marketplace changes a column (and they do), I fix it once.
The server constraint that shaped everything
None of this ran on a beefy machine. The whole pipeline ran on 1 vCPU, 2 GB RAM, no GPU. That forced habits I now keep even on better hardware:
- One process at a time. Never fan out image jobs in parallel on a 1-core box.
- Call
gc.collect()after each asset. - Measure peak RSS with
resource.getrusage(RUSAGE_SELF).ru_maxrss, not average usage. Peak is what kills you. - One file per subprocess for heavy render steps, so memory actually returns to the OS.
- Inspect previews at 60 dpi, render finals at 300. Never the reverse.
The metadata script is trivially light, it never came close to the RAM ceiling. The render steps did, and those habits are what kept the pipeline alive.
Folder structure is part of compliance
A catalogue becomes a junk drawer without fixed paths. The layout this assumes:
project/
assets/ raw output
output/ ready-to-submit files + metadata.csv
packs/ finished product zips + covers
research/ notes and sources
scripts/ the tools
Keep research/ forever. I deleted mine once to "clean up" and lost the provenance notes for a batch of assets. If you're submitting AI-assisted work, knowing which prompt and source produced which file is not optional, it's the difference between answering a review question confidently and guessing.
Where this ended up
That Adobe rejection in March turned into a system that has since packaged 39 finished products, about 856 MB of output, from roughly 19,000 lines of pipeline code on that same 2 GB box. The four scripts in that system are make_metadata.py, upload_tracker.py, md2pdf.py, and pack_product.py. Two third-party packages total: Pillow for covers, reportlab for PDFs. The metadata and tracker scripts need nothing but Python 3.9.
I put the scripts and the rules they enforce, backed by 300+ pages of research, into a product I call the compliance metadata kit. It's the tool I built from this exact failure, sold as-is, no framework and no cloud account. If your bottleneck is the metadata and tracking layer rather than the art itself, it will save you the two-minute rejection email I got.
Top comments (0)