I still have the rejection email open in a browser tab from four months ago. Adobe Stock bounced a batch of 40 images because my CSV had six columns instead of five. Not the images. The spreadsheet. I had spent a weekend generating and reviewing that batch, and the whole upload died at the header row. That was the moment I stopped trusting my own pipeline to be right just because it was automated.
Here is what I actually do now. The scripts handle the boring parts, but I kept four human checkpoints in front of every upload. These are the ones that saved me from a second rejection, and the reasons behind each one are specific to how these platforms actually behave.
Checkpoint one: the CSV header, character by character
Adobe Stock wants a five-column CSV. Vecteezy wants four. Dreamstime wants fifteen. Get one column wrong and the upload fails before a single image is reviewed. There is no partial credit and no useful error message, just a rejection that costs you a day.
So before anything goes out, I open the CSV and count the columns myself. I do not trust the script blindly. make_metadata.py writes the exact header each platform demands, and nothing else, but I still open the file once per batch. Thirty seconds of looking beats a week of waiting for a review that will never happen.
The other thing I check: titles. If I run the script without a titles file, it title-cases the filename and uses that as the title. That is fine for a first pass, and I say so in the tool docs, but it is not fine to submit. "asset-047" turned into "Asset 047" is an instant quality flag with a human reviewer. I fill in real titles before the batch leaves my machine, every time.
Checkpoint two: the double-post check
The fastest way I know to get an account flagged is submitting the same file twice. It happens easily. You run a batch, the upload stalls halfway, you fix something, you run it again. Now you have duplicates in the queue and a pattern a reviewer can see.
I keep a ledger with upload_tracker.py. Before any batch goes out, I run the pending command and read the list with my own eyes. If a filename shows up that I remember submitting last Tuesday, the batch stops right there. The script makes double-posting hard, but the human glance at the pending list is what makes it impossible. Two commands, ten seconds, and I have never had a duplicate since I added this step.
Checkpoint three: the memory ceiling, checked once per session
My whole pipeline runs on 1 vCPU and 2 GB RAM with no GPU. That is not a constraint I fought, it is a constraint I designed around. But it means one bad batch can take the whole box down mid-upload.
My habit here is to check peak RSS, not average usage, before a long run. resource.getrusage(RUSAGE_SELF).ru_maxrss gives you the real ceiling your process has hit. If that number is creeping toward the RAM limit, I do not start the batch. I process one file per subprocess for heavy render steps so memory actually returns to the OS between items, and I call gc.collect() after each asset. I also inspect previews at 60 dpi instead of rendering at 300 when I am just checking composition. Rendering at the resolution you need, not the resolution you might want, is the single biggest RAM saver on a box this small.
One process at a time. On a single core, parallel image jobs do not speed anything up, they just guarantee an out-of-memory kill halfway through.
Checkpoint four: the packaging glance
A zip with a real cover sells. A bare file does not. pack_product.py renders a 1280x720 cover and a 600x600 thumbnail for every product, but I still open each cover before the product goes live.
Why? Because the cover is the only thing a buyer sees before purchase, and the script does not know if the title text overlaps the artwork badly or if the thumbnail is unreadable at small size. I have caught two covers where the subtitle ran off the edge. The script did nothing wrong. The human eye caught what the script could not.
I also keep a fixed folder layout: assets/ for raw output, output/ for ready-to-submit files with their metadata, packs/ for finished zips, research/ for notes and sources. I once deleted a research folder to clean up and regretted it within a month. Research notes are raw material for future products. They stay now, permanently.
Why I did not automate these away
Every one of these checkpoints costs under a minute. Every one of them prevents a failure mode that costs days: a rejected CSV, a flagged account, a crashed server, an unsellable listing. The math is not close. Automation is for the work that is repetitive and low-consequence. These four checks are repetitive and high-consequence, which is exactly the combination where a human glance still pays.
The pipeline behind all this packaged 39 products, 856 MB of output, on that same 1 vCPU, 2 GB server, backed by about 19,000 lines of pipeline code and 300+ pages of research. The four scripts that did the packaging are the parts worth sharing, and I bundled them into the pipeline starter kit: make_metadata.py for the three platform CSVs, upload_tracker.py for the ledger, md2pdf.py for turning Markdown into a styled PDF, and pack_product.py for zips with covers. Two dependencies total, Pillow and reportlab, and the metadata and tracker scripts run on the standard library alone with no install at all.
If you are building the same kind of pipeline, these are the tools I wish I had before that rejection email.
Building digital products? Free guides & kits for digital product sellers.













