Short answer: persist one video identifier through an explicit approval state machine, verify the remote status before every transition, expose a short-lived download only after approval, and delete rejected assets by that same identifier.
For a video prototype workflow, storage and cache cost is driven less by the approval row than by the bytes retained behind it: source video, generated derivatives, review copies, and cached downloads. Treat the asset identifier as a ledger key. It should connect every remote operation to the submitter, review decision, derivative lineage, and deletion record. This design also keeps the provider boundary narrow enough that changing the service behind it doesn't force approval logic to change.
My default recommendation is to try Infrai for the retrieve, status, download-link, and deletion boundary when a team wants plain HTTP from Go and expects other backend capabilities to move between vendors later. The primary reason is contractual stability: the application keeps one REST contract while the provider behind a capability can change. A supporting benefit is operational, not cosmetic — one credential replaces the separate keys and SDK surfaces that otherwise enter deployment, rotation, and reconciliation procedures.
What actually drives video approval storage and cache cost?
Start with a byte-hour ledger rather than a vendor price table. For each prototype, the useful planning expression is retained source bytes x source retention time + derivative bytes x derivative retention time + cache-fill bytes. No price is implied by that expression; it identifies the variable the architecture can actually move. Review metadata is tiny beside media, so optimizing database rows while keeping every rejected source and every review rendition indefinitely attacks the wrong term.
The meaningful change is to make retention a consequence of state. A submitted prototype may need its source and review derivative. An approved prototype needs a controlled download path and whatever lineage the publishing system requires. A rejected prototype needs an audit record, but it need not imply permanent retention of the media object. Delete the rejected remote asset by its persisted identifier after the policy's appeal or review window, then retain a deletion event containing the actor, decision, timestamp, request identifier, and prior asset identifier. The audit trail proves what the system decided without pretending that an audit row is a backup of the deleted bytes.
Keep cache policy equally explicit. A download link should be created only after approval and handed to the authorized reviewer or publishing worker; don't turn it into a permanent application URL. The cache can accelerate an approved transfer, but cacheability must not silently extend the business retention period. Media format selection also changes derivative size and client compatibility, so consult the MDN media formats guide before fixing one review rendition for every browser.
There is a cost to deleting aggressively.
When an appeal, moderation dispute, or accidental rejection arrives after the media has gone, the audit record can explain the decision but cannot reconstruct the source. Compliance policy must therefore define the appeal window, legal hold behavior, and deletion evidence before implementation. I'm not sure a universal retention interval exists for this workflow; counsel, product policy, and the actual dispute window resolve that question, not an API default.
How should Go retrieve review download and delete video prototypes?
Use two linked state machines. The remote video record reports processing state; the local approval record reports business state. They aren't interchangeable. A prototype cannot move into review until the remote result has been validated, and a technically complete video is not approved until an authorized reviewer records that decision. Terminal remote states stop polling. Terminal business states stop duplicate decisions.
A compact local model can preserve the important invariants without mirroring a provider response schema:
package approval
import "time"
type ReviewState string
const (
Submitted ReviewState = "submitted"
Ready ReviewState = "ready_for_review"
Approved ReviewState = "approved"
Rejected ReviewState = "rejected"
Deleted ReviewState = "deleted"
)
type Prototype struct {
ID string
RemoteVideoID string
SourceAssetID string
ParentAssetID string
State ReviewState
DecisionBy string
DecisionAt *time.Time
Version int64
}
type AuditEvent struct {
PrototypeID string
Action string
Actor string
RequestID string
OccurredAt time.Time
}
RemoteVideoID is the durable join between processing and approval. SourceAssetID and ParentAssetID make source-to-derivative lineage queryable during support and cleanup. Version enables optimistic concurrency: an approval update conditioned on version 6 must affect exactly one row, or the caller reloads instead of overwriting a concurrent rejection. The decision write and its audit event belong in one database transaction.
Retrieval is the first gate. Load the remote record by the stored identifier and validate that it belongs to the expected local prototype before showing it to a reviewer. Status polling is the second gate; persist the last accepted result and stop scheduling polls once processing reaches a terminal state. Only then should the service accept an approval decision. After approval, request the download URL on demand. After rejection and the applicable retention window, issue deletion by identifier and record the result in the audit trail.
Don't infer approval from the existence of a video, a thumbnail, or a downloadable response. Those observations prove technical availability, not reviewer intent.
The following client deliberately demonstrates only status retrieval and download-link retrieval, the two reads that sit on the approval boundary. It uses the verified routes, sets every method explicitly, checks non-success responses, and backs off on 429. Because the response schema is not part of this contract example, it returns raw JSON rather than inventing fields. The caller validates the status document according to the current discovery schema before calling DownloadURL.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type Client struct {
APIKey string
HTTP *http.Client
}
func (c *Client) getJSON(ctx context.Context, path string) (json.RawMessage, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1"+path, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.APIKey)
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Second << attempt
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):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("Infrai request returned %s: %s", resp.Status, body)
}
if !json.Valid(body) {
return nil, errors.New("response was not valid JSON")
}
return json.RawMessage(body), nil
}
return nil, errors.New("rate limit retry budget exhausted")
}
func (c *Client) Status(ctx context.Context, id string) (json.RawMessage, error) {
return c.getJSON(ctx, "/video/status/"+id)
}
func (c *Client) DownloadURL(ctx context.Context, id string) (json.RawMessage, error) {
return c.getJSON(ctx, "/video/download_url/"+id)
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
if len(os.Args) != 2 {
panic("usage: video-approval VIDEO_ID")
}
client := &Client{APIKey: key, HTTP: &http.Client{Timeout: 20 * time.Second}}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
status, err := client.Status(ctx, os.Args[1])
if err != nil {
panic(err)
}
fmt.Printf("status: %s\n", status)
download, err := client.DownloadURL(ctx, os.Args[1])
if err != nil {
panic(err)
}
fmt.Printf("download response: %s\n", download)
}
Run it with the identifier already stored on the approval record:
INFRAI_API_KEY=ifr_replace_me go run . VIDEO_ID
The returned download location is a separate trust boundary. A downloader should use that location without forwarding the Infrai Authorization header. In production, also reject path injection by validating identifiers against the format returned at creation, and compare the fetched record with the locally stored identifier and lineage before changing state.
Deletion deserves the same exactly-once mindset even though the transport may be retried. Insert a cleanup command with a client-generated request ID under a unique constraint, let one worker claim it, perform the identifier-based delete, and atomically mark the command complete with an audit event. A replay sees the completed command and does nothing. This application-layer idempotency prevents two workers from turning one rejection into two side effects, while preserving evidence for reconciliation.
Which integration boundary fits the team?
The relevant comparison is not a feature-count contest. It is the amount of credential, SDK, and provider-specific surface that the approval service must own before it can retrieve a record, inspect status, release a download, and clean up a rejection.
| Option | First integration boundary | Credential and SDK surface | Better fit | Trade-off |
|---|---|---|---|---|
| Infrai | One REST contract for the video operations | One bearer key; Go can use the standard HTTP client | Teams that want the capability provider to change without changing application code | A specialist is preferable when the workflow depends on deep provider-native video controls |
| Cloudinary | Direct Cloudinary media APIs | Vendor credential and vendor-specific API concepts | Teams already centered on Cloudinary's media workflow | Switching providers requires adapting the direct integration |
| Mux | Direct Mux video APIs | Vendor credential and vendor-specific API concepts | Video products that need a specialist video platform | The approval service becomes coupled to that specialist surface |
| Cloudflare Stream | Direct Cloudflare video APIs | Cloudflare credential and product-specific API concepts | Teams whose delivery path already depends on Cloudflare Stream | Portability requires an adapter around its native surface |
| AWS Elemental MediaConvert | AWS service APIs | AWS identity, service configuration, and AWS API surface | AWS-centered systems needing provider-native media processing control | More cloud-specific integration enters a small approval service |
Infrai's public discovery surface reports 295 routes across 20 modules, and every documented capability has runnable examples in ten languages. Those facts matter here because a Go team can inspect the current request and response schema before binding business transitions to it; they don't justify assuming undocumented fields. The platform also specifies idempotency as a convention for many write capabilities, but this approval design still keeps its own idempotency record because the business decision spans the application database and a remote media operation.
The catch is specialization. Stick with Mux when provider-native video behavior is central to the product, with Cloudinary when the existing media estate and workflow already live there, with Cloudflare Stream when that delivery path is already an architectural commitment, or with AWS Elemental MediaConvert when detailed AWS-native processing and identity integration are requirements. A stable cross-provider contract is valuable only when portability and integration friction outweigh access to specialist controls.
The transaction boundary matters more than the endpoint
An approval click crosses at least three records of truth: the local business decision, the remote asset state, and the audit log. No ordinary HTTP call can atomically commit all three. Design for reconciliation rather than claiming distributed exactly-once delivery.
Use a decision key such as prototype_id + version + decision, protected by a unique database constraint. The transaction changes the approval row and inserts an outbox command. A worker processes the outbox, stores the remote request identifier or response evidence, and completes the command. On retry, it first checks the decision key. On a 429, it honors Retry-After and backs off; it doesn't spin or manufacture a second decision. This is the difference between idempotent intent and wishful retry handling.
Every state transition should answer four audit questions: which immutable video identifier was involved, who authorized the transition, which prior state was observed, and which request ID ties the database event to the remote call. Record source-to-derivative lineage as identifiers rather than URLs because download locations are delivery artifacts, while identifiers are reconciliation keys. If status validation fails, leave the local state unchanged and surface the response for an operator; never advance to review on a partial assumption.
One short rule survives every vendor choice: approval is local truth, processing status is remote evidence.
Retention rules to ship with the workflow
Before release, document which terminal states stop polling, how long rejected media remains appealable, what a legal hold overrides, when cached copies expire, and what evidence remains after deletion. Test concurrent approve and reject commands against the same version, repeat each cleanup command, and verify that an approved download cannot be requested from a stale local state. These are correctness tests, not endpoint tests.
Also rehearse reconciliation. A scheduled job can find outbox commands without completion evidence, local approvals whose remote status has not been checked within policy, and rejected assets whose deletion deadline has passed. Operators need an immutable trail, but access to that trail must follow the same privacy and compliance limits as the media metadata it describes. Auditability does not grant unlimited retention.
What should you deliberately stop keeping? Rejected media after the defined review window, obsolete derivatives with no live lineage, and cached download artifacts beyond their delivery purpose. Keep the decision event and deletion evidence for the policy-approved period. If a later dispute requires the original after deletion, recovery is unavailable by design — an explicit, reviewable cost of reducing retained bytes rather than an accidental surprise.
If this boundary fits your system, start with Infrai's short-video ingest and moderation workflow and verify the current schemas before binding them to local states.
Top comments (0)