I still have the rejection email open in a tab from March. 47 assets submitted to Adobe Stock, all bounced in one review pass. The CSV header was wrong. Not the art, not the keywords, the header. Adobe's upload template has exactly 5 columns and I had invented my own 6-column version because it "made more sense" to me. The reviewer does not care what makes sense to you. The template parser saw a header it did not recognize and rejected the batch before a human ever looked at it.
That was the day I stopped treating metadata as an afterthought and started building the pipeline that eventually packaged 39 finished products, 856 MB of output, running on a 1 vCPU, 2 GB RAM server with no GPU. Here is what actually worked.
The CSV header is a contract, not a suggestion
Every stock platform has its own fixed upload format, and each one validates the header before anything else.
Adobe Stock wants a 5-column CSV. Vecteezy wants 4 columns. Dreamstime is the wide one, 15 columns. Get the header wrong on any of them and the upload fails instantly, before your files are even evaluated. There is no partial credit.
So the first script I wrote, make_metadata.py, does one thing: it writes the exact header each platform demands and nothing else. You point it at a folder of files, pick the platform, and it emits the CSV in their format. Filenames get title-cased into titles by default, which is fine for a first pass. But I want to be honest here: filename-derived keywords are weak. Adobe reviewers do reject for thin or irrelevant keywords, so replace the defaults with real titles and keywords before you submit. The script accepts a titles.json for exactly that reason.
The workflow is three commands, one per platform:
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
No third-party packages. This one runs on the standard library alone, which matters when your server has 2 GB of RAM and you are counting every dependency.
Double-posting is the fastest way to get flagged
Here is a mistake I nearly made twice. You run a batch upload at night, the connection drops halfway, and in the morning you cannot remember which files went through. So you re-run the batch. Now some assets exist twice on the platform, and duplicate submissions are exactly the kind of behavior that gets an account reviewed or flagged.
The fix is boring and essential: a local ledger. upload_tracker.py keeps a record of what you have already marked done. Before every upload session you run pending to see what is actually left, you mark items done as they succeed, and status gives you the full picture. A re-run physically cannot double-post because the tracker stands between you and the batch.
It uses the standard library only. No database, no install. A text ledger you can read and back up.
The packaging layer is where products actually get made
Making the art is the fun part. Turning art into a product is four separate chores, and each one stalls people:
- A doc that needs to become a sellable PDF.
md2pdf.pyconverts Markdown to a styled PDF with real headings, tables, lists, and links, in one command. It uses reportlab, one of only two third-party packages in the whole kit. - A zip that looks like a product.
pack_product.pybundles the files and renders a 1280×720 cover plus a 600×600 thumbnail. A listing with a real cover sells. A bare zip file does not. This one uses Pillow, the second and last dependency.
Two packages total for the whole kit. The metadata and tracking scripts need nothing at all. On a memory-constrained box, that is not a stylistic preference, it is a constraint.
The server habits that made 2 GB work
Everything above ran on 1 vCPU, 2 GB RAM, no GPU. It works because of habits, not hardware:
- One process at a time. Never fan out image jobs in parallel on a single core. It does not go faster, it just thrashes.
-
Free memory between items. Call
gc.collect()after each asset. Python will not always do it for you promptly. -
Measure peak RSS, not average.
resource.getrusage(RUSAGE_SELF).ru_maxrsstells you the real ceiling you have to stay under. Average usage lies. - One file per subprocess for heavy render steps, so memory is actually returned to the OS when the step ends.
- Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 is the difference between fitting in RAM and swapping.
The folder layout that saved me from myself
One structural decision did more than any script: a fixed layout. assets/ for raw output, output/ for ready-to-submit files with their CSVs, packs/ for finished zips and covers, research/ for notes and sources, scripts/ for the tools.
The one I got wrong first: I deleted my research/ folder during a "cleanup" and lost the raw material for three products I had planned. Scrape logs and notes are future inventory. Keep them forever.
Where this came from, honestly
This is not a theory post. The scripts I described packaged 39 products, 856 MB of finished output. They sit at the packaging layer of a pipeline with about 19,000 lines of code behind it, and the platform rules they enforce came out of 300+ pages of research into how Adobe, Vecteezy, and Dreamstime actually review and reject submissions. Validated on that same 1 vCPU, 2 GB RAM, no GPU box.
I put the four scripts together as the Pipeline Starter Kit because I kept rewriting them for every new product batch and finally admitted they were a product themselves. If you are at the stage where your art is done and the CSVs and packaging are what is stalling you, this is the tool built from exactly that problem.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)