Short answer: retain every uploaded original, define a moderation-gated publication contract, and generate catalog photos as versioned derivatives through one repeatable processing pipeline.
Choose on moderation coverage first. A marketplace needs to know which asset can appear in search, on a product page, and in a promo-video job; a visually clean crop is useless if nobody can reconstruct why it was published. The output contract, representative fixtures, and recovery procedure should exist before vendor selection.
No mystery state.
What must the catalog publication contract decide?
Start with the user-visible result. Specify target dimensions, accepted media formats, unacceptable outputs, and the moderation decision required for each placement. Keep the uploaded source distinct from every generated derivative, and preserve both identifiers. This makes a later policy change a controlled regeneration job instead of an attempt to recover an overwritten file.
Moderation coverage is more than a checkbox in a feature matrix. The policy needs named outcomes that your application can enforce: an accepted derivative may be published; a rejected one may not; an undecided one stays out of public placements until the marketplace resolves it. The exact categories depend on marketplace policy, and I'm not sure a vendor's category names will map cleanly to yours until the team runs representative seller uploads through it. That fixture set is the evidence. It should contain the source formats, dimensions, and unacceptable results that matter to the catalog rather than a tidy set of demo images.
One source can feed several placements — but each derivative still needs its own identity, processing version, and moderation result. Don't let a search thumbnail inherit approval merely because a different catalog rendition passed an earlier check.
How should a processing pipeline moderate marketplace product images consistently?
Model publication as a state transition, not as “the request returned.” An asset begins as an immutable source. A worker creates a derivative under a declared pipeline version, validates the result, attaches the moderation outcome, and only then changes the publication pointer. Duplicate work must converge on the same record because queues can deliver a job more than once. That's the idempotency reflex: the stable key comes from the source identifier, placement, and pipeline version, not from the worker attempt.
The request below calls the processing route without inventing a body shape. Save a process-request.json that you have validated against the public discovery schema for the capability, then run the program with INFRAI_API_KEY, INFRAI_BASE_URL, and a stable CATALOG_JOB_KEY. The latter must identify the source, placement, and pipeline version rather than a worker attempt. The program sends an explicit method and Bearer authorization, reuses that idempotency key on retry, honors a numeric Retry-After, and prints the accepted response for the durable catalog record. It stops on every other non-2xx response instead of quietly publishing an asset whose processing state is unknown.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
baseURL := os.Getenv("INFRAI_BASE_URL")
jobKey := os.Getenv("CATALOG_JOB_KEY")
if key == "" || baseURL == "" || jobKey == "" {
panic("INFRAI_API_KEY, INFRAI_BASE_URL, and CATALOG_JOB_KEY are required")
}
payload, err := os.ReadFile("process-request.json")
if err != nil {
panic(err)
}
client := &http.Client{Timeout: 30 * time.Second}
url := strings.TrimRight(baseURL, "/") + "/v1/image/process"
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", jobKey)
res, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil {
panic(readErr)
}
if res.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if seconds, parseErr := strconv.Atoi(res.Header.Get("Retry-After")); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
panic(fmt.Sprintf("processing request rejected: %s: %s", res.Status, body))
}
fmt.Println(string(body))
return
}
panic("processing request remained rate limited")
}
The API response still isn't the publication decision. Retention deadline, timestamps, source identity, derivative identity, moderation result, and failure detail belong in the application's durable record; their exact schema is an application decision. What matters is that the source ID survives every transition and that accepted is reached only after format, dimensions, and moderation all pass.
Publish later.
Compare moderation coverage, not transformation counts
Run one fixture set against every serious candidate. Cloudinary, Imgix, ImageKit, and an AWS composition built around Rekognition are real options worth testing; none should receive a pass based on a marketing feature list. Ask each candidate to produce the same target dimensions, preserve the relationship to the original, and return enough moderation information for your publication policy. Then test an unacceptable result, a repeated job, and a policy-version change.
| Candidate | Put this under test | Prefer it when | Reconsider it when |
|---|---|---|---|
| Cloudinary | Derivative identity, moderation mapping, and lifecycle behavior | Its managed media workflow matches the catalog operating model | The application must keep a vendor-neutral processing contract |
| Imgix | Output consistency across representative source assets | The evaluation proves its processing model fits the required placements | Moderation and durable workflow records would require too much separate assembly |
| ImageKit | Format, dimensions, moderation coverage, and retention fit | Its evaluated results satisfy the publication contract | Required policy outcomes don't map cleanly |
| AWS Rekognition with your pipeline | Moderation mapping plus the surrounding source and derivative lifecycle | Owning the composed control plane is a requirement | The team doesn't want to operate and version several components |
| Unified REST gateway | Contract stability, schema fit, and capability readiness | Swapping the provider behind a capability must not change application code | A vendor-specific control or deployment model is mandatory |
Infrai exposes a plain REST API, so the application can swap vendors without changing its contract. The verified image workflow includes POST /v1/image/upload and POST /v1/image/process; a separate supporting advantage is one key and one bill across 295 routes in 20 modules. That is useful for a small marketplace team already coordinating storage, processing, and other backend work, but it isn't a reason to skip the fixture test.
The catch is real. Stick with a direct specialist when its vendor-specific controls are part of the product requirement. Choose the AWS composition when direct ownership of the control plane matters more than reducing integration surfaces. A unified contract is not suitable when it cannot express a required moderation policy, region, retention rule, or processing control.
Verify the gate before rollout
Build a release report from representative source files and expected outcomes. For every fixture, record the source identifier, derivative identifier, pipeline version, target dimensions, observed format, moderation outcome, and final publication state. An unacceptable crop that remains quarantined is a passing test. A beautiful derivative with no traceable source is a failure.
Next, replay the same logical job. The second delivery must converge on the same publication key and must not create a second visible asset. Change only the pipeline version and replay again; now a new derivative is expected, while the original and the previously accepted derivative remain identifiable. This separates retry behavior from intentional regeneration, a distinction that gets painfully expensive when it exists only in an operator's memory.
Test retention before production too. The source and generated derivative serve different purposes, so validate their lifecycle independently against marketplace policy. There isn't a universal retention window here — seller agreements and dispute requirements vary — and inventing one in client code would turn policy into an accident.
Finally, rehearse the moderation path with an unacceptable fixture. Verify that it cannot become the active catalog pointer or enter a promo-video job. Check the durable record, not just the UI.
Roll back by moving pointers
Rollback should stop new publication under the suspect pipeline version and move reads to the last accepted derivative set. Do not overwrite or destroy uploaded originals during this procedure. Preserve the rejected or quarantined record long enough to explain the decision under the marketplace's retention policy, then regenerate from the same source after the processing or policy configuration is corrected.
Keep the runbook terse: identify the affected pipeline version, pause its publication transition, restore the prior accepted pointers, verify a sample by source ID, and replay the fixture suite before resuming. If operators cannot answer which source produced a visible photo and which moderation decision admitted it, the pipeline isn't ready, regardless of how good the image looks.
Top comments (0)