I lost a whole afternoon once to a single row of CSV headers. I had 40 JPEGs ready for Adobe Stock, generated the metadata file with a script I'd hacked together the night before, uploaded it, and got an instant rejection. Not a review rejection. A parse rejection. The file never even made it to a human. The problem was one column: I had written Keywords where Adobe wanted keywords, lowercase, and I had eight columns where Adobe's ingest expects exactly five. That was the day I stopped guessing and wrote the header check into the script itself.
Here is what I learned getting metadata CSVs to pass on the first try, across Adobe, Vecteezy, and Dreamstime.
Adobe wants exactly five columns, in this order
Adobe Stock's CSV ingest is strict. The header row must be five columns: Filename, Title, Keywords, Category, Releases. Get the spelling, the order, or the count wrong and the upload fails before review. I keep all three platform headers as constants in make_metadata.py so the script writes the header the platform demands and nothing else. No extra convenience columns, no notes column, no trailing empty field. Adobe's parser does not forgive a sixth column even if it is empty.
The category column takes a number, not a name. Adobe maps categories to integers, so the script writes the digit and I keep the mapping in one place. If you hand-type category names you will get silent mis-categorization at best.
Vecteezy is four columns and less forgiving about keyword count
Vecteezy's header is four columns: filename, title, keywords, category. Lowercase. The trap here is not the header, it is the keywords field. Vecteezy has a keyword limit, and if you paste in the same 49-keyword blob you use for Adobe, you get flagged or truncated. I keep per-platform keyword caps in the script and trim to the strictest one per platform rather than writing one CSV and hoping.
The other trap: keywords must be space-separated inside the cell, not comma-separated. Adobe takes commas. If you reuse the Adobe file for Vecteezy, every keyword becomes one long garbage string. This is exactly the kind of mistake a re-run with the wrong --platform flag makes, which is why the script takes the platform as a required argument and writes to a differently named file.
Dreamstime is fifteen columns and the only one with a real release field
Dreamstime's CSV has fifteen columns, which sounds scary until you realize most of them stay empty for vector and illustration work. The important ones are the filename, title, keywords, category, and the release fields. If you submit work containing recognizable people or property, the release columns are where a "no" or an editorial flag matters. For pure AI-generated abstract and decorative work, most release columns stay blank, but the header still has to list all fifteen or the file bounces.
The lesson across all three: the header is a contract, not a suggestion. I stopped hand-editing CSVs entirely. The script reads a folder of files, title-cases the filename as a placeholder title, and writes the exact header for the platform I name.
Filenames and titles do the heavy lifting before review
A correct header gets your file parsed. The title and keywords decide whether it passes review and gets found. My first-pass titles were filename garbage like asset_001_final.jpg title-cased, which is useless. The script accepts a titles.json file so I can supply real titles and keywords per asset, and I treat the filename-derived title as a placeholder only. Every asset gets a human-written title and 25 to 35 relevant keywords before submission. Anything I skipped that step on came back from review or sat unseen.
Double-posting is the failure mode nobody warns you about
Once the CSV passes, the new danger is running your batch twice. Uploading the same asset twice is one of the fastest ways to get an account flagged on every major stock platform. I built upload_tracker.py after doing exactly this once: a ledger that records what is done, so pending shows only what has not been posted, and a re-run cannot double-post. Three commands, standard library only, no database. pending before every batch, done after each upload, status whenever I am unsure.
The rest of the pipeline runs on almost nothing
All of this ran on a 1 vCPU, 2 GB RAM server with no GPU. Metadata generation is trivial compute, so it is pure Python standard library, no installs. The heavy steps elsewhere in the pipeline follow two rules: one process at a time on a single core, and gc.collect() after each asset. Measuring peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss told me the real memory ceiling instead of what I guessed it was. If you are generating AI images, the cost is per image and small, but it adds up across a catalogue, so I batch and review before rendering finals.
What I actually ship now
My folder layout is fixed: assets/ for raw output, output/ for submission-ready files plus the CSVs, packs/ for finished products, research/ for notes, scripts/ for tools. The metadata step is one command per platform. The tracker keeps the ledger. Nothing gets hand-edited, because hand-editing is where the eight-column mistake came from in the first place.
I packaged this into the Pipeline Starter Kit: the four scripts (make_metadata.py with the exact Adobe, Vecteezy, and Dreamstime headers, upload_tracker.py, md2pdf.py, pack_product.py) that turned one prompt engine into 39 finished products, about 856 MB of output, on that same 2 GB box. Two dependencies total, and the two metadata scripts need none at all. If you are assembling your own pipeline, it is the packaging layer, not a course.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)