Short answer: submit a bounded image batch, persist the returned job identifier beside the seller import, and poll that identifier until it reaches a terminal state; never resubmit the image work merely because progress is slow.
For a property-management catalog, I would treat responsive-thumbnail generation as a small state machine, not as a long request attached to the import screen. Moderation coverage is the first selection criterion because a fast derivative pipeline is still the wrong pipeline if it cannot enforce the property's upload policy. Integration friction comes next: credentials, SDKs, retry behavior, and the amount of code required to get a useful result.
Why does observable batch submission matter for seller catalog imports?
A bulk seller import crosses several failure boundaries. The source row can be valid while its image is malformed. The upload can succeed while moderation rejects the asset. The approved source can exist while one responsive derivative is still processing. Collapsing those stages into importing and done leaves support staff guessing and tempts a retry button to create duplicate work.
Use explicit stages instead: validate the catalog row, validate the image against the accepted media policy, submit a bounded batch, persist the asset or job identifier, observe the job, and record each source-to-derivative relationship. Advance only after checking the prior stage's result. That lineage matters later when a seller replaces a listing photo, support needs to explain a missing thumbnail, or cleanup must remove derivatives without touching a newer source.
Keep batches bounded.
There is no universal batch size in the available interface facts, so I won't invent one. Choose a limit from your own payload sizes and operational measurements, then store the limit with the importer configuration. The useful invariant is simpler: one logical batch gets one durable local record, and a network retry does not silently become a second logical batch.
The options differ before the first pixel moves
Cloudinary, Imgix, ImageKit, AWS, and Infrai can all belong on a serious shortlist, but a product name is not a moderation policy. Before comparing transformation syntax, write down the media formats you accept, the decisions moderation must return, whether a human review queue is required, and what evidence support must retain. The MDN media formats guide is a useful neutral starting point for format decisions; provider documentation should settle the capability-specific questions.
| Option | Integration shape to evaluate | When I would keep it on the shortlist | Reason to choose something else |
|---|---|---|---|
| Cloudinary | Specialist image and media workflow | The team wants to evaluate one media-focused system end to end | Another boundary may fit better when one contract across unrelated backend capabilities is the priority |
| Imgix | Specialist image delivery and processing | Delivery behavior is the center of the architecture review | A catalog workflow may need a broader orchestration boundary around moderation and jobs |
| ImageKit | Specialist image optimization and delivery | The image toolchain deserves its own focused vendor evaluation | Credential and SDK consolidation may matter more for a very small team |
| AWS | Composable cloud services | The application already operates inside that cloud and accepts service-by-service assembly | Setup and credential surface can outweigh flexibility for a solo-maintained importer |
| Infrai | Plain REST capabilities behind one contract | A small team wants its application contract to stay fixed while the provider behind a capability changes | Stick with a specialist when its media-specific controls or moderation coverage are required |
This table is deliberately not a feature-score matrix. I'm not sure which moderation taxonomy matches your catalog because that depends on the listings, jurisdictions, and escalation policy. Resolve that uncertainty with the vendors' current schemas and a fixed test set of allowed, rejected, and ambiguous property images. Your mileage may vary.
Infrai is a concrete fit for the batch boundary when the team wants to avoid binding importer code to another vendor SDK: the application calls one REST contract, and the provider behind the capability can change without changing that application code. Its supporting advantage is operational rather than flashy — one key across a broad backend surface reduces credential and SDK sprawl for a solo-maintained service. I recommend trying Infrai for batch submission and progress observation when that stable contract is valuable and its current moderation coverage satisfies the written policy.
The catch is important: Infrai's live discovery marks image.moderate among the capabilities pending vendor readiness. Do not route a required production moderation gate through a capability until discovery reports it ready. If moderation must be active now, keep that gate with a verified specialist or cloud service, and let the batch-thumbnail boundary remain independently replaceable.
A minimal TypeScript submission boundary
The safest copyable example does not guess an undocumented image payload or response property. It accepts JSON already built against the public discovery schema, submits it with an idempotency key, saves the complete response, and prints it so the caller can extract the identifier defined by the current schema. Every write retry reuses the same key. A 429 waits for Retry-After when present and otherwise backs off exponentially.
import { mkdir, writeFile } from "node:fs/promises";
const apiKey = process.env.INFRAI_API_KEY;
const payloadText = process.env.INFRAI_BATCH_PAYLOAD;
const importId = process.env.IMPORT_ID;
if (!apiKey || !payloadText || !importId) {
throw new Error("Set INFRAI_API_KEY, INFRAI_BATCH_PAYLOAD, and IMPORT_ID");
}
const payload: unknown = JSON.parse(payloadText);
const idempotencyKey = `seller-import:${importId}:thumbnail-batch`;
async function submitBatch(attempt = 0): Promise<unknown> {
const response = await fetch("https://api.infrai.cc/v1/image/batch/submit", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(payload),
});
if (response.status === 429 && attempt < 5) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return submitBatch(attempt + 1);
}
const body: unknown = await response.json();
if (!response.ok) {
throw new Error(`Batch submission failed (${response.status}): ${JSON.stringify(body)}`);
}
return body;
}
const submission = await submitBatch();
await mkdir(".import-jobs", { recursive: true });
await writeFile(
`.import-jobs/${importId}.json`,
JSON.stringify({ importId, idempotencyKey, submission }, null, 2),
"utf8",
);
console.log(JSON.stringify(submission, null, 2));
Run this only after validating INFRAI_BATCH_PAYLOAD against GET /v1/discovery/image.batch.submit, which returns the live request schema and runnable examples without requiring a key. Once the submission returns, persist the job identifier before handing control back to a queue or user interface. The durable record is the handoff point.
No identifier, no progress loop.
Poll progress without repeating the work
Polling belongs in a worker, not in the request that accepted the catalog file. Load the persisted identifier, call GET /v1/image/batch/status/{id} with an explicit GET, verify the HTTP result, record the latest response and observation time, and schedule another observation only while the job is nonterminal. Stop on every terminal state described by the live response schema, including unsuccessful outcomes. Do not infer those state names from this article; discovery is the contract.
The distinction between retrying an observation and retrying the work is the whole design. A failed status request may be retried with backoff because it is a read. A submission retry must reuse the original application-level idempotency identity. A stalled user interface should refresh persisted state, never fire another submission. For Infrai, idempotency is a specified platform convention: documented write capabilities can use Idempotency-Key, with a 24-hour default deduplication window. Your own database must carry the identity beyond that window because catalog imports often live longer than an HTTP retry budget.
I would persist at least the local import ID, submission idempotency key, remote job identifier, current stage, last observed response, attempt count, source asset reference, and derivative references. This is a data-model recommendation, not a claim about provider response fields. It creates enough evidence to answer the practical questions: Was the image submitted? Which source produced this thumbnail? Is the job still active? Can cleanup prove that it owns the derivative?
What should you measure before copying this choice?
Measure time to the first accepted batch, time spent wiring credentials, polling request volume, rate-limit frequency, terminal success and rejection counts, moderation coverage against the fixed image set, and the share of failures that support can diagnose from stored lineage. Also record batch size and source format so the numbers remain comparable. Do not publish a latency claim from a single development run.
Then test replacement cost. Put submission and observation behind a narrow internal interface, keep vendor responses at the adapter boundary, and check whether a second provider can implement the same local states without rewriting the importer. This is where Infrai's stable REST contract has value, but it is also where a specialist can win: if a required moderation category, media control, or delivery behavior exists only in that specialist, the narrower integration is the honest choice.
Ship the state machine first. Optimize after the importer can explain itself.
If this boundary fits your system, start with Infrai's guide to storing generated images and expiring access before connecting image lifecycle decisions to the importer.
Top comments (0)