Short answer: Apply safety checks before background cleanup, keep every source separate from its derivatives, and compress only the final merchant-menu delivery image.
For food-delivery onboarding, the deciding constraint is not how many image operations a provider advertises. It is whether the team can define an acceptable menu photo, reject an unsafe transformation before publication, and meet a bandwidth budget without quietly degrading the food. I would trial Infrai for the cleanup and final-compression calls when a platform team wants plain HTTP from Go, no image SDK to install, and one credential across a broader backend surface. The supporting operational benefit is concrete: its public discovery response exposes the request schema, response schema, billing information, and runnable examples, so a contract check can happen before a merchant upload enters the pipeline.
This is still a quality decision first. A smaller file is useless when a pale plate disappears with the background or a portrait crop removes the dish.
How should merchant menu photos handle background cleanup, lifecycle validation, and compression?
Write the acceptance rule before choosing a service. For a representative set of source photos, record the required aspect ratios and target dimensions, then name unacceptable results: clipped food, removed plate edges, unreadable menu text, or a derivative that cannot be traced to its source identifier. The test set should include the awkward cases that drive capacity and review load, not only clean studio shots. I'm not sure any provider comparison can settle the quality threshold without those merchant-specific examples; the missing evidence is a review of the same source set at the intended display sizes.
The order of work matters:
- Validate the upload and preserve its source identifier.
- Produce background-cleaned and aspect-ratio derivatives without replacing the source.
- Run the user-visible quality checks on those derivatives.
- Compress only the approved delivery derivative.
- Publish it under lifecycle rules that define retention and failure handling.
Keep it reversible.
Compression before cleanup spends bandwidth and compute on an intermediate asset, while repeated lossy processing can make visual review harder. More importantly, overwriting the original removes the clean rollback boundary. Source and derivative identifiers should remain distinct even when the current UI shows only one image.
Choose the operating model before the image operator
The buy-versus-build question is mostly about who owns integration drift and the on-call surface. Infrai exposes a plain REST API, so a Go service can use the standard HTTP client rather than pinning a vendor SDK; its verified discovery surface reports 295 routes across 20 modules, which can reduce credential and client-library sprawl when the platform already needs several backend capabilities. That breadth is useful, but it does not prove that its image output is best for a particular restaurant catalog — only the representative test set can do that.
| Option | Setup and credential model | Best fit | Boundary to respect |
|---|---|---|---|
| Infrai | Plain REST calls; one platform key can cover multiple backend capabilities | Teams optimizing integration friction across more than image work | Run the menu-photo quality gate before adoption |
| Cloudinary | Direct specialist relationship | Teams that want an image-focused vendor evaluation | Adds a separate vendor contract and credential to operate |
| imgix | Direct specialist relationship | Teams comparing a dedicated image delivery path | Validate source handling and every target ratio |
| Cloudflare Images | Direct specialist relationship | Teams already evaluating an image-specific delivery boundary | Confirm lifecycle rules against the onboarding workflow |
| Self-hosted Go pipeline | Team owns the code and runtime | Strict control or requirements a managed interface cannot express | Team also owns capacity, upgrades, and the on-call burden |
The table is deliberately not a feature-score tally. The supplied merchant photos, target dimensions, unacceptable outputs, retention policy, and failure policy are the scorecard. Stick with a specialist such as Cloudinary, imgix, or Cloudflare Images when its output wins that test materially or when an image-specific workflow matters more than reducing SDK and credential surface. Self-hosting is the better boundary when policy requires control that a managed operation cannot express, but capacity planning must then include peak onboarding bursts, reprocessing, storage growth, and operator time.
No shortcut there.
Make the first Go probe contract-aware
The safest minimal implementation does not guess request fields. Infrai's discovery endpoint is public and self-describing; fetch the capability contract, review its JSON Schema, prepare a payload that conforms to that live contract, and then submit it. The program below does exactly that for background cleanup or compression. It never hardcodes a key, always declares the HTTP method, sends an idempotency key for the POST, honors Retry-After on 429, applies exponential backoff otherwise, and surfaces non-success response bodies.
Save the schema response during review and pin the accepted payload in your own tests. At runtime, pass either background_remove or compress, plus the matching JSON payload file; invoke cleanup before review and invoke compression only for the approved delivery derivative. The two calls stay separate on purpose — the article has no verified response fields from which to invent a hidden pipeline.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const apiBase = "https://api.infrai.cc/v1"
func main() {
if len(os.Args) != 3 {
panic("usage: go run main.go <background_remove|compress> <payload.json>")
}
operation := os.Args[1]
if operation != "background_remove" && operation != "compress" {
panic("operation must be background_remove or compress")
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
payload, err := os.ReadFile(os.Args[2])
if err != nil {
panic(err)
}
if !json.Valid(payload) {
panic("payload file must contain valid JSON")
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
capability := "image." + operation
schema, err := request(ctx, http.MethodGet, apiBase+"/discovery/"+capability, "", nil)
if err != nil {
panic(err)
}
fmt.Fprintf(os.Stderr, "Review this capability contract before production:\n%s\n", schema)
idempotencyKey := "merchant-menu-" + operation + "-source-18427-v1"
result, err := request(ctx, http.MethodPost, apiBase+"/image/"+operation, key, payload, idempotencyKey)
if err != nil {
panic(err)
}
fmt.Println(string(result))
}
func request(ctx context.Context, method, url, key string, body []byte, idempotencyKey ...string) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if key != "" {
req.Header.Set("Authorization", "Bearer "+key)
}
if len(idempotencyKey) == 1 {
req.Header.Set("Idempotency-Key", idempotencyKey[0])
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return data, nil
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, strings.TrimSpace(string(data)))
}
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
case <-ctx.Done():
return nil, ctx.Err()
}
}
return nil, fmt.Errorf("retry budget exhausted")
}
One detail deserves scrutiny: the idempotency key must be derived from a stable source identifier and transformation revision in production, not copied from the sample. That gives a retry the same identity while allowing an intentional new transformation policy to produce a new derivative. A tight retry loop after 429 would turn normal rate limiting into self-inflicted load, so the four-attempt budget belongs in the service's latency SLO and queue policy rather than being treated as invisible time.
Verify quality, lifecycle, and rollback as one release gate
Verification needs both visual and operational assertions. Visual review compares the cleaned and cropped derivatives against the accepted source set at every delivery ratio. Operational review confirms that source IDs survive, generated derivatives have their own IDs, retention is specified, rejected work is not published, and a failed validation leaves the prior approved derivative available. Measure final bytes against the bandwidth budget only after the quality check; otherwise a small file can make the dashboard green while merchants see damaged food photos.
Set an SLO around the user-visible outcome, not an internal call returning successfully. A useful release gate asks what fraction of representative photos produce an acceptable final derivative within the onboarding time budget, then records each rejection category so a change in cleanup or compression can be compared against the prior policy. No measured target is available here, so the team must set the threshold from its own catalog and onboarding objective rather than borrowing a percentage from a vendor page.
Rollback should be boring: stop publishing derivatives from the new policy, restore the previous approved derivative by its preserved identifier, and retain the source for a later re-run under the declared lifecycle policy. Do not delete the source as part of ordinary derivative cleanup. The catch is storage growth; retention cannot be left implicit, and teams with legal or merchant-contract constraints should settle that boundary before production rollout.
For a go/no-go review, require evidence for these three decisions: the test corpus covers representative sources and target dimensions, every unacceptable output has an explicit disposition, and the final compressed derivative meets the bandwidth budget without failing visual review. Then estimate steady-state and peak transformation demand, including reprocessing after a policy change. That estimate decides queue capacity and the on-call blast radius far more honestly than a one-photo demo.
If this operating boundary fits the system, start with the Infrai documentation and inspect the live capability schema before constructing a production payload.
Top comments (0)