KovAPIKovAPI
POST https://kovapi.com/v1/videos/generations

Google Veo 3.1 video API: 8-second clips with sound, 720p / 1080p, landscape or portrait, billed per clip. Text-to-video, first frame, first + last frame and up to 3 reference images. Async jobs; failures are not billed.

Try in Playground →

Quick start

Three steps: submit a job, poll it, take the video link. Replace YOUR_API_KEY with your key and the commands below run as they are.

Step 1 · Submit (returns a job id at once, HTTP 202)

curl
curl https://kovapi.com/v1/videos/generations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "veo-3.1", "prompt": "A cat surfs a wave at sunset, the sea glittering gold, the camera tracking from the side", "resolution": "720p", "size": "16:9"}'

Response (HTTP 202)

JSON
{
  "id": "img_5c2e8f1a9b3d7e6f0a4c8b2d",
  "object": "video.generation.job",
  "status": "queued",
  "created": 1791281400,
  "model": "veo-3.1"
}

Step 2 · Poll every 5–10 seconds until status is succeeded or failed

curl
curl https://kovapi.com/v1/videos/jobs/img_5c2e8f1a9b3d7e6f0a4c8b2d \
  -H "Authorization: Bearer YOUR_API_KEY"

Response when it succeeds

JSON
{
  "id": "img_5c2e8f1a9b3d7e6f0a4c8b2d",
  "object": "video.generation.job",
  "status": "succeeded",
  "created": 1791281400,
  "model": "veo-3.1",
  "data": [
    {"url": "https://img.kovapi.com/i/8c1f0e5d2b7a49e3a6d0c4f1b9e2a7d5.mp4"}
  ],
  "usage": {"clips": 1, "model": "veo-3.1"}
}

Step 3 · data[0].url is the clip (MP4). It is kept for 15 days; download it to your own storage to keep it longer.

Complete example (Python: submit, wait, save to disk)

Python
import time
import requests

BASE = "https://kovapi.com/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}

# 1. submit
job = requests.post(f"{BASE}/videos/generations", headers=HEADERS, json={
    "model": "veo-3.1",
    "prompt": "A cat surfs a wave at sunset, the sea glittering gold, the camera tracking from the side",
    "resolution": "720p",
    "size": "16:9",
}).json()
if "error" in job:
    raise SystemExit(job["error"]["message"])

# 2. poll every 10 seconds, for up to an hour
deadline = time.time() + 3600
while job["status"] not in ("succeeded", "failed") and time.time() < deadline:
    time.sleep(10)
    job = requests.get(f"{BASE}/videos/jobs/{job['id']}", headers=HEADERS).json()

# 3. download the video
if job["status"] == "succeeded":
    url = job["data"][0]["url"]
    with open("veo-3.1.mp4", "wb") as f:
        f.write(requests.get(url).content)
    print("saved", url)
elif job["status"] == "failed":
    print("failed:", job["error"]["code"], job["error"]["message"])
else:
    print("still", job["status"], job["id"])

While queued, the poll also returns queue_position (jobs ahead of yours) and eta_seconds (expected wait). If your time limit passes, do not resubmit: the job is still there, so keep polling the same id later.

Authentication

Header
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

Create an API Key in the console to start.

At a glance

Duration8 s · fixed length
Resolution720p / 1080p
Aspect ratio16:9 / 9:16
Reference imagesup to 3 · one as the first frame, two as first + last frame (720p only), three as references
Reference videosnot supported
Reference audionot supported
Soundalways on; cannot be switched off (audio: false is refused)
Prompt lengthup to 20,000 characters (any script counts the same)
Billingper clip: one price per clip

Request parameters

The JSON body of POST /v1/videos/generations:

ParameterTypeRequiredDescription
modelstringYesThe model id: veo-3.1.
promptstringYesWhat happens in the clip: picture, camera and motion. Up to 20,000 characters (any script counts the same).
durationintegerNoAlways 8 seconds: omit it or send 8.
tierstringNoQuality tier: fast only (the default); may be omitted.
resolutionstringNoResolution: 720p / 1080p; 720p when omitted.
sizestringNoOrientation: 16:9 / 9:16; 16:9 when omitted. Alias aspect_ratio.
imagesarrayNoImages, up to 3: one becomes the first frame; two become first + last frame (720p only); three are references. A single image may go in image.
image_rolestringNoHow images are used: first_frame (first frame, or first + last frame; 2 images at most) or reference (1–3 references: the people or style appear in the clip without being a frame). When omitted, 1–2 images are frames and 3 are references.
audiobooleanNoEvery clip comes with sound and it cannot be switched off; false is refused (400), so leave it out.
callback_urlstringNoWhere to push the result when the job ends; see Webhooks below.
nintegerNoEach job makes one clip; leave it out. A value above 1 still makes one clip, but n clips are held at submit and the extra is returned when the job ends.

Other fields (seed, negative_prompt, fps, watermark and the like) are not passed on and do not change the price; results are always MP4, returned as a url.

Price

Billed per clip: one price per clip whatever its length; only the resolution can change it. Held at submit, refunded in full on failure. 1 credit = $0.005.

ResolutionCredits / clipUSD / clip
720p40$0.20
1080p44$0.22

Reference material

  • No image is text-to-video; one image is the first frame; two are first + last frame (720p only); three images, or 1–3 with image_role: reference, are references: their people or style appear in the clip without being a frame.
  • No reference video or audio. Every clip is 8 seconds with sound.
  • You can refer to an image in the prompt as @Image1.
  • Each image is a public image URL, a data: URI or base64, up to 16 MB; send several as an array in images. We store the images before the job is queued; one that cannot be read is refused with 400, message says which, and nothing is charged.
  • Reference files we store are kept for 7 days; results for 15 days.

First + last frame (720p)

curl
curl https://kovapi.com/v1/videos/generations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "veo-3.1", "prompt": "A flower bud slowly opens in front of the camera", "images": ["https://example.com/first.png", "https://example.com/last.png"], "resolution": "720p", "size": "16:9"}'

The two images become the first and last frame; 720p only. To use 1–3 images as references, add "image_role": "reference".

Job status and fields

GET /v1/videos/jobs/{id} reports these statuses; every field is described in Async jobs. Poll every 5–10 seconds, or let a webhook (below) push the result. Polling is free, and any key of the same account can read the job.

StatusMeaning
queuedWaiting in line. Carries queue_position (jobs ahead of yours) and eta_seconds (expected wait).
processingRendering.
succeededDone. The result is in data[0].url, the billing detail in usage.
failedFailed. The reason is in error; the credits held for the job are refunded in full.
  • processing can go back to queued, for example while the service is busy, while a finished clip is fetched again, or after a restart. If a job cannot be finished, the gateway may render it again automatically; however many retries it takes, a job is charged once.
  • One account runs up to 50 jobs at a time; further jobs wait in submission order, and a job that has not started after 15 minutes fails and is refunded in full. Job records are kept for 3 days and video links for 15 days.

How long it takes

Usually 1–3 minutes.

Errors and refunds

An error at submit comes back at once as HTTP 4xx / 5xx: no job is created and nothing is charged. A failure after the job was accepted is reported by the job endpoint, and the hold is refunded in full. Every code is listed under Error codes.

Errors at submit

HTTPcodeCauseWhat to do
400invalid_requestA parameter is not valid: the duration, resolution, ratio or number of references is outside this model's range, the prompt is too long, a reference image or video cannot be read, audio: false was sent to a model whose clips always have sound, and so on. message says which.Fix what message says and submit again.
400invalid_callback_urlcallback_url is not a public http(s) address, or is longer than 2000 characters.Use an address that can be reached from the internet.
400model_not_foundThe model id is misspelled, model is missing, or the model cannot be used on this endpoint (a chat model on the image endpoint, for example).Use a model id from GET /v1/models or the API reference.
401invalid_api_keyThe key does not exist, was deleted or is disabled, or the account is disabled.Check the key on the API Keys page of the console.
402insufficient_quotaNot enough balance to hold this request. The gateway holds the highest possible cost of the request first, so a burst of concurrent calls can trigger it too.After a burst, wait a few seconds and retry; otherwise top up.
429rate_limit_exceededOver the requests-per-minute limit set on this key (counted over the last 60 seconds). No Retry-After header.Wait a few seconds and retry, or raise the key's limit in the console.
503model_maintenanceThe model is under maintenance and takes no requests; no other model answers in its place meanwhile.Retry when maintenance ends; see Status.
503service_unavailableA temporary failure on our side, such as storing a reference file. No job was created and nothing was charged.Retry shortly.

Failed jobs (status failed)

codeWhen you see itWhat to do
content_policyThe prompt or a reference image did not pass the model provider's content review.Rewrite the prompt or replace the reference before you submit again; the same input is refused again.
invalid_requestThe model provider does not accept this combination (a mode and a resolution, for example). message says why.Adjust the parameters as message says and submit again.
wait_timeoutVideo: no result within 120 minutes, so the job timed out and its credits were refunded in full; if the video finishes later, it is delivered free.Submit again.
upstream_errorAny other failure: the automatic retries did not help, the job waited 15 minutes without starting, the upstream refused without a reason, and so on; message says which.Submitting again usually works.

Branch on code, not on the text of message. Treat any code you do not recognise (or a missing one) as upstream_error: resubmitting usually works. Every failed job is refunded in full.

Webhooks instead of polling

Add callback_url when you submit and the moment the job ends, success or failure, the gateway pushes exactly what the poll endpoint returns to that URL, signed and retried on failure. You can also set an account-wide address under Settings → Webhook in the console. Webhook docs →

curl · callback_url
curl https://kovapi.com/v1/videos/generations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "veo-3.1", "prompt": "A cat surfs a wave at sunset, the sea glittering gold, the camera tracking from the side", "resolution": "720p", "size": "16:9", "callback_url": "https://your-domain.com/webhook"}'