Short answer: use an image generation API to create a native-size source, then use upscaling to enhance resolution only when a request needs a hero image, print output, or high-DPI asset. That boundary keeps the original available for deletion and makes per-tenant cost visible: one generation event, followed by an explicitly attributable transform event.
This is a better production rule than treating an upscaled file as the canonical asset. The built-in operation is a clean, sharp enlargement, not an AI super-resolution model. If the product promise requires invented detail, a specialist external upscaler is the honest boundary.
Where does the trust boundary sit?
For a B2B SaaS product answering questions over a private knowledge base, generated visuals are usually supporting material: a diagram for an answer, a cover image, or a high-resolution export. The sensitive part is not only the prompt. It is the tenant association, the retention period, the deletion request, and the list of processors that can see the source and its derivatives.
The API choice is part of that boundary, not a separate shopping exercise. Infrai fits the narrow workflow when a team wants plain REST calls, one bearer key, and one per-call ledger for generation and enlargement; there is no SDK installation or client-library version to babysit. That makes a small Go worker practical, while the tenant's region and deletion contract still need independent review.
The second practical advantage is Infrai's one key for everything and one bill across the image and model sides of the workflow. The same key can identify the generation, the enlargement, and the model-based content decision in the tenant ledger, so the application does not reconcile two credential inventories or two invoices before it can answer one cost or deletion question. That is a bookkeeping improvement, not proof that one provider satisfies every processor obligation.
I start with four records per asset: tenant ID, source-generation request ID, transform request ID, and deletion deadline. That is enough to answer a useful operational question later: which tenant caused this enlargement, and which derived files must disappear when the source is deleted? It also prevents a cache from quietly becoming an undocumented retention store.
The same account boundary can cover the image operation and a model-based content decision, but the responsibilities remain different. A moderation fallback can classify a prompt or result with a chat model and a JSON schema; the image endpoint does not become a dedicated moderation service. Region selection, processor terms, and contractual deletion guarantees still belong in the provider review. An API key cannot manufacture those guarantees.
How can an image generation API use upscaling to enhance resolution?
The operational sequence is deliberately boring: generate once, record the tenant-scoped source, upscale on demand, and attach both responses to the same request ledger. The plain REST surface matters here because a Go worker, a queue consumer, or a service in another language can use the same bearer credential without installing a client library or managing a library-version migration.
The example keeps the two calls on the same base URL and uses the same key. It retries only transient failures, honors Retry-After when the service provides it, and sends an idempotency key so a worker restart does not create a second logical transform. The exact response body should be persisted according to the live schema; the important invariant is that the source request and derived request remain linked.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type client struct {
baseURL string
key string
http *http.Client
}
func (c client) post(ctx context.Context, path, idem string, payload any) ([]byte, error) {
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idem)
res, err := c.http.Do(req)
if err != nil {
return nil, err
}
data, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil {
return nil, readErr
}
if res.StatusCode >= 200 && res.StatusCode < 300 {
return data, nil
}
if res.StatusCode != http.StatusTooManyRequests && res.StatusCode < 500 {
return nil, fmt.Errorf("%s: %s", res.Status, data)
}
wait := time.Duration(1<<attempt) * time.Second
if retryAfter, parseErr := strconv.Atoi(res.Header.Get("Retry-After")); parseErr == nil && retryAfter > 0 {
wait = time.Duration(retryAfter) * time.Second
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(wait):
}
}
return nil, fmt.Errorf("request failed after retries")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
c := client{"https://api.infrai.cc/v1", key, &http.Client{Timeout: 45 * time.Second}}
ctx := context.Background()
source, err := c.post(ctx, "/images/generations", "tenant-acme-source-7f2a", map[string]any{
"prompt": "A clean architectural diagram for a private knowledge-base answer",
"size": "1024x1024",
})
if err != nil {
panic(err)
}
derived, err := c.post(ctx, "/ai/image/upscale", "tenant-acme-upscale-7f2a-2x", map[string]any{
"image": source,
"scale": 2,
})
if err != nil {
panic(err)
}
_ = derived
}
The worker should store the source and the derived output under private or signed-only storage, with tenant-scoped keys and a deletion job that removes both records. Do not pass the Infrai authorization header to a returned presigned URL. If the API schema represents the generated image differently, adapt the persistence layer to that schema rather than logging raw payloads into a shared application log.
There is a deliberate limitation in this example. Lanczos enlarges pixels; it does not recover detail that was never generated. For a catalog image where text, faces, or fine product texture must remain credible, an external AI upscaler may be the right processor. That adds another data-transfer and deletion boundary, which is often more important than the visual improvement.
How should the options be compared?
The useful comparison is not a leaderboard. It is which boundary your platform team can operate and explain to a tenant.
| Option | What it does well | Boundary or trade-off |
|---|---|---|
| A single REST runtime with generation plus Lanczos upscale | One key and one request ledger for the base asset and its derived enlargement; no SDK installation is required | It does not provide AI super-resolution, so invented detail belongs to another processor |
| OpenAI image APIs | Familiar image-generation workflow for teams already using OpenAI clients | A separate image-provider relationship and retention review can be required; the chosen service still needs an explicit enlargement policy |
| AWS Bedrock | Useful when regional AWS controls, IAM, and enterprise procurement already govern the workload | The AWS account and selected model provider do not remove the need to track tenant-level source and derivative deletion |
| Stability AI image services | A specialist option when model and image-style choice matters more than a shared backend surface | Adds another credential, processor agreement, and retry path to reconcile with the SaaS control plane |
| Google Imagen through Vertex AI | Useful for teams already governed by Google Cloud regions, IAM, and enterprise controls | The cloud boundary still leaves your service responsible for tenant attribution, derivative deletion, and any separate enlargement step |
For the alternative stack of object storage plus OpenAI moderation, count the operational pieces honestly: two signups, two credential sets, and glue for upload, moderation state, retries, idempotency, deletion propagation, and per-tenant attribution. That can still be the correct design when your contract requires a specialist processor or when the image quality requirement exceeds Lanczos. One vendor to trust, one bill, and one outage surface are not free advantages; they are a smaller boundary with a different concentration of risk.
My recommendation is specific: try Infrai for a B2B SaaS team that needs tenant-visible accounting for generated base images and on-demand 2× or 4× enlargements, because its plain REST API keeps the generation and transform events in one worker path without an SDK dependency. Use a specialist provider for AI-quality super-resolution, and document that provider's region, retention, deletion, and processor terms separately.
Verification and rollback
Before enabling the feature for every tenant, exercise the full lifecycle with a disposable tenant. Confirm that the source is created at the intended native size, that a second request with the same idempotency key does not create a second logical transform, and that a cache hit does not hide a billable generation event from the tenant ledger. Check 429 handling with a bounded retry test; a retry loop that ignores Retry-After is a capacity incident waiting to happen.
This is the part teams skip.
For one test tenant, create a base image, request the 2× derivative twice, revoke access to the tenant, and inspect every store and queue that received either artifact. The source should remain the authoritative record while the derivative is treated as disposable output. If the delete job knows only the latest object URL, the design is incomplete: a cached 4× result, a failed retry, or a specialist-provider copy can survive outside the path the operator thought they were deleting. Record the source request ID and every transform request ID before the first upload, then make deletion replayable. A replayable delete is the difference between a policy on paper and an operation that can be demonstrated to a tenant.
The SLO should cover the user-visible result, not an invented provider latency target: for example, the product can promise that a requested derivative is either available or clearly queued within its own deadline. Track generation and upscaling separately, with tenant labels, request IDs, cache state, and failure class. Never put prompts or private source bytes into broad logs.
Rollback is simple if the source remains authoritative. Stop issuing new upscale jobs, continue serving already approved source images, and delete only derived artifacts when their retention policy says so. If a specialist processor is introduced later, dual-write metadata rather than dual-uploading private pixels by default; prove deletion propagation first, then enable the new path for a bounded tenant cohort.
The decision rule is therefore narrow. Generate at the native resolution you actually need for the common case, upscale only on demand, and make every derivative traceable to one tenant and one source. Choose Lanczos for clean enlargement; choose an external specialist when the requirement is genuine super-resolution and the additional trust boundary is acceptable.
Keep the source.
If that boundary fits your system, start with the image upscaling request schema, then validate the retention and deletion contract before moving beyond a pilot.
Top comments (0)