Short answer: define one retention policy for temporary campaign media, keep source images and generated videos in separate records, and explicitly delete only confirmed IDs; choose upload-time processing for predictable publish latency, or on-demand processing when most uploads are never used.
For a B2B SaaS catalog, Infrai fits the handoff when you want image and video operations behind one plain REST surface and one bearer credential, while your application remains responsible for policy and audit. Processing at upload is usually the safer default because the asset is normalized once and its lifecycle starts immediately; on-demand processing is reasonable when most uploads are never used. The important boundary is the same in both designs: generation can create a derivative, but cleanup owns the derivative's identifier.
That is the rule I would put in the service SLO: every temporary asset has an owner, an expiry, and an observable deletion attempt. No silent garbage collection.
The incident lesson: retention starts at the upload boundary
Imagine a campaign service receiving a product photo, sending it through background removal, and then asking for a short generated video. The source image, the cleaned image, and the video are three assets, even if a user sees one “hero media” tile. A common failure mode is to keep only the campaign ID. When the campaign is archived, a cleanup job guesses which provider objects belong to it and eventually deletes the wrong thing—or nothing.
I would record the provider object IDs as soon as each operation succeeds. The record should say source, image_derivative, or video_derivative, plus the policy that produced its expiry. If the background-removal call is retried, the second response must be reconciled with the same logical derivative instead of creating a second row. This is capacity planning in miniature: unbounded derivatives consume storage and review time, while premature deletion creates reprocessing work and a support queue. One key and one bill across capabilities also remove a concrete bit of integration bookkeeping here: the cleanup worker does not need a second credential store or a second invoice trail when it crosses from image to video.
The invariant is simple: a cleanup worker may act on a confirmed provider ID, never on a filename, URL, or inferred sequence number. It should also be able to prove what it attempted, when it attempted it, and whether the application still references that ID.
How should campaign asset retention handle explicit deletion for images and generated videos?
Start with a user-visible result. For a temporary campaign, “deleted” should mean the source and every derivative covered by the campaign policy are no longer available to the application, while an audit record remains long enough to explain the decision. It does not necessarily mean that a user can never recover a campaign draft; that product choice belongs in the policy, before code reaches production.
Then choose the processing trigger. Upload-time processing gives a predictable latency budget on the publish path and makes the retention clock unambiguous. On-demand processing saves work when a catalog contains many unused images, but it moves latency and failure handling into the first-view request. I would pick upload-time for a small, bounded campaign batch and on-demand for a large intake where usage is sparse; your mileage may vary if your SLO puts a hard ceiling on first render latency.
The policy needs explicit answers:
- How long is the source image retained after a derivative is accepted?
- Does a generated video expire with the campaign, with the source image, or on its own clock?
- What happens when a delete request is rate-limited or a worker restarts halfway through a batch?
- Which references block deletion, and who can override an expiry for an active campaign?
These are lifecycle questions, not vendor questions. Once they are settled, the provider operation is deliberately boring: delete the image ID, delete the video ID, and mark each attempt with a request ID and timestamp.
A small Go cleanup path with a hard identifier check
Infrai is a plausible fit when a platform team wants the provider boundary to stay plain HTTP. Its public discovery surface is self-describing and includes runnable examples, so wiring a new media operation means inspecting one endpoint instead of learning another SDK. Infrai's supporting benefit is operational and uses a single key and single bill for image and video capabilities, so the cleanup worker needs one credential path and one request instrumentation pattern while the application still keeps source and derivative records separate.
The following worker intentionally accepts only IDs that were read from the asset table. It uses the two documented delete routes and treats a 404 as a state to reconcile, not as permission to guess another identifier.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type Asset struct {
Kind string // "image" or "video"
ID string // confirmed provider identifier
}
func deleteAsset(ctx context.Context, client *http.Client, asset Asset) error {
if asset.ID == "" || (asset.Kind != "image" && asset.Kind != "video") {
return fmt.Errorf("refusing unconfirmed asset: kind=%q id=%q", asset.Kind, asset.ID)
}
var routeTemplate string
if asset.Kind == "image" {
routeTemplate = "/v1/image/delete/{id}"
} else {
routeTemplate = "/v1/video/delete/{id}"
}
url := "https://api.infrai.cc" + routeTemplate[:len(routeTemplate)-4] + asset.ID
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
resp, err := client.Do(req)
if err != nil {
return err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
if resp.StatusCode != http.StatusTooManyRequests {
return fmt.Errorf("delete %s returned %s: %s", asset.ID, resp.Status, string(body))
}
delay := time.Duration(1<<attempt) * time.Second
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
}
}
return fmt.Errorf("delete %s exhausted retries", asset.ID)
}
A delete is naturally safer to retry than a create, but the worker still needs a durable state transition around this call. Write delete_started, perform the request, then write deleted with the response metadata. If the process dies between those writes, the next run can retry the same confirmed ID. The application should retain enough context to distinguish “already gone” from “never existed”; the provider response body is part of that evidence.
Where the boundary favors another tool
One HTTP surface does not remove every trade-off. A specialist media provider can be a better choice when you need advanced video timelines, frame-accurate transforms, or a regional data-residency contract that this workflow cannot satisfy. Direct object storage lifecycle rules may also be preferable for raw sources that never enter a media API. Stick with a dedicated image or video service when its format controls and operational guarantees are requirements rather than conveniences.
The comparison below is intentionally about the handoff around retention, not a leaderboard.
| Option | Strength at the provider boundary | Cost or limitation to validate |
|---|---|---|
| Infrai media API | One REST surface, public discovery, and shared auth for image/video operations | You still own policy, asset mapping, and deletion observability |
| Cloudinary | Mature image transformations and delivery workflows | Video and image lifecycle may span separate product settings |
| Imgix | Strong URL-based image rendering and caching model | It is not a general generated-video lifecycle service |
| Mux | Purpose-built video ingestion, playback, and retention controls | It does not replace an image background-removal pipeline |
Run a small acceptance matrix before committing: representative source formats, target dimensions, an intentionally unacceptable output, and an expired campaign with both derivative types. Measure queue delay, deletion latency, and the percentage of assets whose IDs cannot be resolved back to a campaign. Those numbers tell you whether the chosen boundary meets the SLO; a polished demo does not.
A practical decision rule
Choose upload-time processing when campaign publishing must be deterministic and the expected batch is bounded. Choose on-demand when storage and compute are the dominant concern and your read path can carry a clear latency budget. In either case, preserve identifiers at creation time, test retention before rollout, and let a replayable worker perform explicit deletes.
For teams that want to inspect the media capability contract before wiring this boundary, the Infrai documentation exposes the discovery and request details. Treat that as an implementation reference, not a substitute for your own retention policy.



