I still have the rejection email open in another tab. Batch of 60 vectors, three weeks of generation work, and Adobe came back with rejections on almost all of them. The reasons looked random at first. "Title quality." "Keywords." "Similar content." I spent a weekend reading every single rejection reason I had ever received, sorting them into a spreadsheet, and I found something annoying: most of them trace back to one rule about metadata, not about the art.
The rejections cluster into one bucket
When I actually counted, the rejections fell into a small number of groups. Quality rejections on the images themselves were real and I deserved those. But the majority were metadata problems wearing different costumes. A title that reads like a keyword dump. Keywords that do not match what is visibly in the image. A filename that got auto-promoted into a title because I never replaced it.
That last one was my own fault and it is the most common mistake I see other makers make. My pipeline used to title-case the filename and call it a title. "Blue Geometric Pattern 042" is not a title, it is a filename with spaces. Adobe's reviewers see hundreds of these a day and they reject them, correctly.
The fix was boring. I stopped letting the pipeline invent titles. Now there is a titles.json file, one real title and one real keyword set per asset, written by a human who looked at the image. The CSV builder reads that file if it exists. If it does not exist, the filename fallback still runs, but I treat that as a first pass only, never a submission pass.
Every platform has a header, and it is not the same header
The second thing the rejection spreadsheet taught me: a huge share of "invalid file" rejections are just the wrong CSV header. Adobe wants a 5-column header. Vecteezy wants 4 columns. Dreamstime wants 15. Submit Adobe's format to Vecteezy and it fails before a human ever sees your work.
I used to keep three spreadsheets and copy columns between them by hand. That is how double-posts and mismatched rows happened. So I wrote one script, make_metadata.py, that takes a folder of files and emits the exact header each platform demands, nothing else. One folder, three commands, three correct CSVs:
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
The header is not configurable on purpose. If the platform changes its format, I change the script. If I let myself "improve" the CSV with extra columns, I am back to instant rejections.
The rejection nobody warns you about: running your batch twice
This one cost me real review goodwill. I re-ran an upload batch after a partial failure, forgot which items had already gone through, and double-posted a chunk of the catalogue. Duplicate submissions are the fastest way I know to get an account flagged, and no metadata script protects you from it because the CSV is fine, you are the problem.
The fix is a dumb ledger. upload_tracker.py is a plain file that remembers what you have already posted:
python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
pending shows only what has not gone out yet. A re-run now physically cannot double-post, because the tracker sits between my batch list and my upload queue. It is standard library only, no install, and it has prevented more account damage than anything else in the pipeline.
The rejection before the rejection: your machine giving up
While fixing the metadata side I also had to make the generation side survive, because half my early batches died mid-run. The whole thing runs on 1 vCPU and 2 GB RAM with no GPU, so the rules that keep it alive are strict:
- One process at a time. Never fan out image jobs in parallel on a single core.
- Call
gc.collect()after every asset. - Measure peak RSS with
resource.getrusage(RUSAGE_SELF).ru_maxrss, not average memory, because the peak is what kills you. - Run heavy render steps one file per subprocess so memory actually returns to the OS.
- Inspect previews at 60 dpi instead of rendering at 300 just to check composition.
A batch that used to die at asset 30 now runs to the end. Every asset that survives the run is an asset that never produces a "server error, resubmit" cycle.
What I actually learned from all those emails
Read as a pile, the rejections felt personal. Read one by one, they were almost all the same lesson: the platforms do not reject art as often as they reject sloppiness around the art. Wrong header, invented title, duplicate submission, half-finished batch. The art itself only needed to clear a quality bar I could meet with fewer, better prompts.
Once the metadata, tracking, and packaging layers were automated, the same prompt engine that produced mess turned into 39 finished products, about 856 MB of packaged output, built on roughly 19,000 lines of pipeline code and 300+ pages of research notes. The notes live in a research/ folder I never delete anymore. Deleting scrape logs to "clean up" is a mistake I made exactly once.
I packaged the four scripts that did this, the same ones that ran on that 2 GB box, into a small kit. It is not a course and it does not generate art for you. It is the packaging layer: the metadata builder for all three platforms, the upload tracker, the Markdown to PDF converter, and the product packager that zips files and renders a 1280x720 cover and 600x600 thumbnail. Two dependencies total, Pillow and reportlab, and the metadata and tracking scripts need nothing but Python.
Building digital products? Free guides & kits for digital product sellers.
Top comments (1)
tr.ee/dev-to