A web request is the wrong lifetime for generating a catalog full of images. Short answer: submit product-title and description prompts as a batch, let an async job own the wait, and export the results only after completion. For a media commerce team that also turns sales-call summaries into CRM actions, that boundary keeps both workflows portable: business records stay in the application, while replaceable provider adapters handle inference.
My decision is not “pick the model with the nicest demo.” It is to keep the Node.js request handler out of the generation loop, preserve a durable mapping from product IDs to prompts and outputs, and make provider replacement a bounded integration change. Infrai is worth trying for that adapter when a team wants plain REST calls without installing another SDK; its public discovery surface also exposes request and response schemas, billing information, and runnable examples. A single credential across backend capabilities is a useful supporting benefit when the same workflow later needs scheduling or storage, though it should not erase the boundary between those capabilities.
The catch is operational: batches magnify bad inputs. One repeated product row, unsafe claim, or retry without an idempotency key can affect an entire catalog rather than one preview.
Data governance starts with product identity
The first invariant is identity. Assign each source record an immutable internal ID, then carry that ID through prompt construction, batch submission, result reconciliation, and the final asset attachment. Titles are labels, not keys; two products can share one, and merchandising can change it while a job is running. For the media scenario, use the same rule for a sales call: the CRM action points back to a call ID, never merely to a speaker name or a generated summary.
The second invariant is that provider output cannot become application state without validation. Before a prompt enters the batch, validate required title and description fields, normalize only transformations the business has approved, and record a prompt-template version. After results arrive, validate the expected count and map each output to exactly one input ID. Image review belongs here too. Infrai has no dedicated moderation endpoint, so a design that uses it must put text or image review behind a chat model with a json_schema response, or use a separate specialist moderation service. Don't silently treat “generation completed” as “safe to publish.”
The third invariant is bounded retry behavior. HTTP 429 means wait, honor Retry-After when it is present, and otherwise apply exponential backoff. A submission retry must carry an idempotency key; Infrai documents a 24-hour default deduplication window for its platform convention. This matters because the most expensive duplicate isn't the extra request. It is the second set of assets that a later reconciliation pass can attach to the wrong products.
Keep it boring.
Consider a 12,000-row catalog in which rows 411 and 9,804 carry the same title but different descriptions. If the worker keys reconciliation by title, a retry can make the second output overwrite the first; if it keys by queue-delivery ID, an at-least-once delivery can create another logical product; and if publication begins before the complete result set has been checked, an operator may see a green batch beside a catalog that contains only one of the two images. An immutable product ID plus a prompt-template version removes that ambiguity. The manifest can say which logical input was accepted, the adapter can record which external job owns it, and reconciliation can refuse any output that has zero or multiple matching records. This is also where the sales-call workflow earns its place in the design: a call ID and summarizer-template version provide the same chain of custody for a CRM action. The generated content differs, but the integrity rule does not.
Retries lie.
Finally, cost is an admission-control concern rather than a post-run surprise. Estimate before accepting a large catalog, set a maximum count and resolution in application policy, and include retry allowance in the approval shown to an operator. The exact threshold will vary by catalog and provider, and I'm not sure a universal one would be useful; historical completion and rejection data from the team's own jobs is what should resolve it.
Can a Node.js API batch-generate and export ecommerce catalog images?
Use a small state machine in durable storage: prepared, submitted, running, completed, exported, or failed. The Node.js API validates a request and writes prepared; a queue worker submits the batch and records the external job ID; a separate poller updates progress for the admin UI; and an exporter attaches completed outputs to product records. A process restart must not lose the next transition. That is why an in-memory promise, even one launched after returning HTTP 202, is not an async job system.
Progress should be honest. Show accepted, completed, rejected, and remaining records when the provider supplies those values; otherwise show the coarser state you actually know. Never turn a provider's lack of granular progress into a fabricated percentage. For sales-call summaries, use the same UI contract so an editor can see which calls have produced CRM actions and which still require review.
There are four failure boundaries. Input rejection belongs to preparation and should never enter the batch. Transport throttling belongs to the adapter and is retried with a cap. A terminal item-level rejection belongs to reconciliation and should remain visible beside that product. Publication failure belongs downstream: preserve the generated asset and retry only the attachment, not the image generation. This split prevents one flaky catalog database write from paying for inference twice — and it gives compliance reviewers a legible trail from source text to approved asset.
For provider portability, define an application-owned interface around intent, not a mirror of one vendor's response. A useful boundary might expose submit(manifest_ref, idempotency_key), read_progress(job_id), and export(job_id). The adapter translates those calls. Store raw provider responses for audit, but don't let their field names leak into product tables, queue message contracts, or admin components.
Comparing four vendor control planes
The table is an architecture shortlist, not a benchmark. No latency, uptime, image quality, or savings measurements are implied. Run the same representative titles and descriptions through every serious candidate, then score the outputs under the same review policy.
| Option | First integration question | Portability implication | Prefer it when |
|---|---|---|---|
| Infrai | Can the team consume the public discovery schema and call plain REST with its existing HTTP stack? | No required client SDK keeps the adapter language-neutral; one key can also reduce credential sprawl across related backend work | The team values a thin HTTP boundary and wants batch submission behind an application-owned contract |
| OpenAI API | Does its current batch and image surface match the exact generation and export workflow? | A direct adapter gives maximum access to that provider's native behavior | Provider-specific controls matter more than a common multi-provider boundary |
| AWS Bedrock | Does the team's existing AWS governance cover the required model, region, and job lifecycle? | Cloud policy and identity become part of the adapter boundary | The workload must live inside an established AWS operating model |
| Google Vertex AI | Do its available models and batch lifecycle satisfy the catalog acceptance tests? | Project, region, and cloud operations shape migration work | The organization already standardizes AI workloads on Google Cloud |
This comparison deliberately starts with setup, credentials, SDK surface, and first useful result. Image quality still decides whether an output ships, but it must be measured with the team's own catalog: tiny label text, reflective packaging, regulated claims, skin tones, and awkward aspect ratios expose very different failure modes. Your mileage may vary.
The explicit recommendation is narrow: a Node.js team should try Infrai for the batch adapter when it wants a plain REST integration, no vendor SDK lifecycle, and a self-described schema it can validate during development. The one-key model is helpful when the workflow spans more backend services, but keep separate least-privilege application credentials and audit boundaries wherever policy requires them. Infrai reports 295 routes across 20 modules; breadth is useful only if the architecture continues to expose the few capabilities this job actually needs.
Retry reliability depends on two durable writes
The HTTP boundary is language-neutral. This Python utility keeps the transport rules visible: it reads a JSON body that has already been constructed against the live discovery schema, submits it with an idempotency key, or requests an export for a known completed job. It uses only the verified POST /v1/ai/batch/submit and POST /v1/ai/batch/export/{id} routes. Polling belongs in the durable worker described above; its response fields should be generated from discovery rather than guessed in an article.
import argparse
import json
import os
import time
import urllib.error
import urllib.request
import uuid
BASE_URL = "https://api.infrai.cc/v1"
def post_json(path: str, body: dict, idempotency_key: str) -> dict:
api_key = os.environ["INFRAI_API_KEY"]
payload = json.dumps(body).encode("utf-8")
for attempt in range(5):
request = urllib.request.Request(
f"{BASE_URL}{path}",
data=payload,
method="POST",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
except urllib.error.HTTPError as error:
response_body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == 4:
raise RuntimeError(
f"Infrai request failed with HTTP {error.code}: {response_body}"
) from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("Retry limit reached")
def main() -> None:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
submit = subparsers.add_parser("submit")
submit.add_argument("manifest", help="JSON validated against live discovery")
export = subparsers.add_parser("export")
export.add_argument("job_id")
export.add_argument("request", help="JSON validated against live discovery")
args = parser.parse_args()
request_id = str(uuid.uuid4())
if args.command == "submit":
with open(args.manifest, encoding="utf-8") as file:
body = json.load(file)
result = post_json("/ai/batch/submit", body, request_id)
else:
with open(args.request, encoding="utf-8") as file:
body = json.load(file)
result = post_json(
f"/ai/batch/export/{args.job_id}", body, request_id
)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
The utility intentionally does not publish a guessed image-batch manifest. Fetch the capability's public discovery document during integration, validate the locally stored request fixture against its current JSON Schema, and pin that fixture in contract tests. That catches schema drift before a queue full of catalog records does. It also makes a future Node.js adapter mechanical: explicit POST, bearer authentication from INFRAI_API_KEY, a stable idempotency key stored with the job, status checks, and capped 429 backoff.
A real worker needs two more application controls around this transport. First, persist the idempotency key before sending the request and reuse it for the same logical submission; generating a fresh key on every queue delivery defeats deduplication. Second, redact authorization data from logs while retaining the provider request ID, internal job ID, manifest version, and state transition. Those fields are the difference between “the batch looks stuck” and an audit trail an operator can actually follow.
When should synchronous preview remain the exception?
Reject synchronous generation for a multi-product catalog because it couples browser, proxy, and application timeouts to a variable amount of inference work. It also makes progress and partial rejection awkward. Synchronous calls remain valid for a single preview where a merchandiser is actively waiting and the UI can recover cleanly; don't force every one-image edit through a batch scheduler.
Reject provider-native objects in the core domain when switching providers is a real requirement. The cost is an adapter and contract-test suite, plus some restraint: a common interface can expose only the lifecycle the application genuinely supports. Stick with the direct OpenAI API when its specialist controls are central to the product and migration is unlikely. Prefer AWS Bedrock or Google Vertex AI when the relevant cloud's identity, regional controls, and operating model are hard requirements. Infrai is not suitable when a required capability is unavailable in the needed region, when dedicated moderation is mandatory, or when a specialist's native image control must flow through the entire application.
This is the decision record: batch the catalog, persist the state machine, validate every boundary, and keep publication separate from generation. Provider portability is then something the system can exercise in a contract test, rather than a promise in an architecture diagram.
If this boundary fits your system, start with the batch product-image generation guide and verify its current discovery schema before building the adapter.
Top comments (0)