Building an AI Video SaaS: What the API Didn't Solve

The first time an AI video API returned a finished clip, I thought I had crossed the hardest part of the project.

I had sent an image and a motion prompt, received a task ID, waited for processing, and displayed the result. The main technical risk seemed solved.

It was not.

A successful generation proved that an API could produce a video. It did not prove that the surrounding product could survive a timeout, a page refresh, an uncertain submission, a pricing change, a duplicated request, or a failed task involving a user's credits.

That distinction changed how I approached the rest of the build. The model remained important, but most of the difficult product work moved to the system around it.

A working API call is only a demo

The happy path for an image-to-video feature looks simple:

  1. Upload an image.
  2. Describe the motion.
  3. Select a model and settings.
  4. Submit the request.
  5. Wait for the finished video.

That flow is useful for proving an integration. It is not enough for a product that accepts money or consumes credits.

The real questions begin around the edges:

  • What happens if the provider accepts the request but the website never receives confirmation?
  • Can an in-progress generation be restored after the user refreshes the page?
  • Does a polling timeout mean the generation failed, or only that observation stopped?
  • Can the same browser action create two tasks and two charges?
  • Will a failed task restore credits exactly once?
  • Is the provider's temporary result URL copied into storage controlled by the product?
  • Can the history page reconstruct what the user actually requested?

None of these questions improves the visual quality of a generated clip. Every one of them affects whether the user can trust the product.

Rows of server racks representing the hidden infrastructure behind an AI video product
The interface is visible. Most of the reliability work happens behind it.

The template gave me speed—and inherited assumptions

I did not begin with an empty repository. I started from an existing SaaS foundation that already included authentication, payments, credits, a database, and administrative features.

That saved a large amount of setup time. It also created a different kind of work.

The template contained an old brand, old pages, old copy, and business assumptions that did not belong to an image-to-video product. At first, changing the logo, domain, and homepage made the project look new. It did not make the product coherent.

The more useful work was deletion:

  • removing pages that no longer served a clear purpose;
  • rewriting inherited copy that promised the wrong thing;
  • separating reusable infrastructure from product-specific behavior;
  • deciding which settings belonged to each video model;
  • replacing template identity with a consistent product contract.

A template can shorten the distance to the first screen. It cannot decide what the product should be.

Long-running generation needs explicit states

Video generation is not an ordinary request-response operation. The provider usually returns a task identifier before the video exists. The product must then observe a process that may take much longer than a normal web request.

I found it more useful to think about generation as a state machine rather than a loading spinner. A task can be pending, processing, successful, failed, or canceled. The transition matters as much as the final state.

A finite state machine diagram illustrating explicit state transitions
A simple state-machine analogy: events should cause explicit transitions instead of leaving the system in an ambiguous loading state.

For each internal generation task, the system needs enough persistent information to answer practical questions later:

  • Which user created it?
  • Which public model and input mode were selected?
  • What prompt and source assets belong to it?
  • Which provider task corresponds to it?
  • How many credits were consumed?
  • Has a terminal result already been processed?
  • Where is the durable output stored?

Once that information is stored, refreshing the page no longer needs to erase the user's mental model of the task. The interface can recover the active generation from the server instead of pretending that nothing happened.

This overview explains durable execution as a general pattern for long-running workflows. My product uses its own task runner rather than Temporal, but the underlying reliability problem is closely related.

Not every retry is safe

Retries sound like a reliability feature. Used without a clear model, they can create duplicate work and duplicate charges.

A status query is often safe to repeat. A generation submission may not be.

Imagine that the provider accepts a POST request and starts a paid task, but the connection closes before the website receives the response. From the website's perspective, the outcome is unknown. Automatically sending the same POST again may create a second provider task.

The system needs a deliberate policy for that ambiguity. In the current product, an unconfirmed submission is not blindly repeated. The task is marked as failed and the consumed credits are restored. This favors a visible, recoverable failure over silently charging for duplicate work.

For operations that are safe to retry, limits and backoff still matter. The AWS Builders' Library explanation of timeouts, retries, backoff, and jitter is useful because it treats retries as a form of load, not free insurance.

The broader lesson is simple: retry behavior is part of the product's money and trust model. It should not be an accidental property of a fetch wrapper.

The browser can display a price, but the server owns the charge

Model pricing initially looked like a configuration label. Then the number of variables grew.

A generation cost can depend on the model, duration, resolution, draft mode, audio, or the number and type of reference images. Different models support different combinations.

If the browser calculates a number and submits that number as the amount to charge, several things can go wrong:

  • an old browser session may use stale pricing;
  • two interfaces may implement the formula differently;
  • a client request can be modified;
  • a promotional display value can be confused with the real billing value.

The safer contract is for the client to submit the user's selected parameters. The server validates those parameters and independently calculates the authoritative credit cost.

const request = validateGenerationRequest(body);
const model = resolvePublicModel(request.modelId);
const creditCost = calculateCreditCost(model, request.options);

await createTaskAndConsumeCredits({
  userId,
  request,
  creditCost,
  idempotencyKey,
});

The browser can still show an immediate estimate. It just does not become the billing authority.

Provider cost, customer credits, and promotions are different facts

Another important separation is the difference between what a provider charges the product and what the product charges a user.

Provider cost is procurement. Customer credits are product pricing. A crossed-out promotional value is presentation.

They influence one another, but they should not be represented by one overloaded field.

  • Provider cost helps determine whether a model is economically sustainable.
  • Customer credit cost is the server-side billing contract.
  • Promotional reference price explains an offer in the interface.

This separation makes supplier changes less dangerous. Replacing a provider or adjusting an internal cost estimate should not silently change what an existing user is charged.

Free access is still a production contract

Free generation can look like a marketing switch: set the price to zero and allow the request.

In practice, it needs the same task integrity as paid generation. The product currently offers signed-in users three daily Video Fast generations without a visible watermark. That allowance still has to pass through authentication, usage limits, task creation, provider submission, result storage, and failure handling.

If the free path bypasses the normal lifecycle, it becomes a second product hidden inside the first one. Bugs then appear only for free users, or only for paid users, because the two paths no longer share the same truth.

I prefer one generation lifecycle with an explicit allowance decision. The billing amount may change, but the task contract should not.

Multiple models need one public language

Adding models creates another boundary problem. Providers use private model identifiers, endpoints, parameters, and status formats. Those details should not leak into every interface.

The public product needs its own model language:

  • a stable public model ID;
  • supported input modes;
  • valid durations, resolutions, aspect ratios, and reference inputs;
  • safe defaults;
  • a server-side mapping to the selected provider implementation.

This lets different pages share the same capabilities without sharing the same layout. A homepage, a focused model page, and a full creation workspace can look different while still agreeing about what the selected model supports and what it costs.

The alternative is slow drift: one selector offers an unsupported duration, another page displays an old price, and the server accepts a combination the interface never intended to expose.

The result also needs a life after the provider response

A provider URL is not automatically a durable product asset. It may expire, change access rules, or remain tied to a vendor-specific response structure.

When a generation succeeds, the product needs to normalize the result, store it under its own task record, and make it available to history, preview, and download surfaces.

The same applies to the input context. A useful history item should preserve the user's prompt and relevant settings—not only an internal provider payload that the user never saw.

This is where an API integration becomes a product record. The result is no longer a temporary response in one browser tab. It belongs to a recoverable user workflow.

AI made implementation faster—and overbuilding easier

I used AI heavily while developing the project. It reduced the time needed to trace code, draft tests, compare implementation options, and carry repetitive changes across a large codebase.

That speed introduced a less obvious risk.

When features become cheaper to implement, every possible feature starts to look reasonable. Another model, another page, another setting, and another abstraction can all appear to be progress.

But implementation speed does not answer product questions:

  • Does this feature strengthen the main use case?
  • Will a user understand why it exists?
  • Does it create another state or pricing path that must be maintained?
  • Am I solving a real constraint, or avoiding distribution and validation?

The bottleneck moved from writing code to making decisions.

A functional product is not a validated product

The product can now take a still image, accept a motion prompt, expose model-appropriate settings, create a long-running generation task, and return a stored video result. That is real progress.

It is not proof of market success.

I am building Image to Video AI as a focused workflow for turning still images into generated videos across multiple models. The system is much more coherent than the template I began with, and the reliability work gives it a stronger foundation.

The next questions are no longer primarily technical:

  • Can the product earn stable, relevant traffic?
  • Do users return after the first experiment?
  • Which image-to-video jobs are important enough to repeat?
  • Is the product's positioning clear enough to justify choosing it?
  • Are users willing to pay for the complete workflow rather than only test a free generation?

Development produces visible artifacts. Validation may initially produce silence. That makes it more uncomfortable, but also more informative.

The lesson I am keeping

The first successful API call generated a video.

The product had to handle everything before and after that call: identity, inputs, capability rules, authoritative pricing, task state, ambiguous failures, retries, refunds, storage, recovery, and history.

Only then did the harder non-technical work become impossible to ignore.

A reliable system gives a product the right to ask the market for attention. It does not guarantee the answer.

This is not a success story. It is the first honest checkpoint.

Disclosure: This article is based on my own product-building experience. AI was used to help organize and edit the English draft, and I reviewed the factual claims against the current product and implementation.

评论