Home › Guides › Kling API with curl

Calling Kling from the command line: create, poll, fetch

Updated 2026-10-02

Before writing any client code, it is worth driving the Kling API by hand once. Three curl commands and a bit of jq show you the whole contract: how a job is created, how it moves through its states, where the finished URL lives, and what an error looks like. Everything below uses VideoRouter's single POST /videos endpoint with Kling v3.0 model ids. If you prefer Python, the Kling v3 Python tutorial covers the same flow.

Step 0: set up your key

Create an account, generate a key, and export it so it never appears in your shell history as a literal:

export LLMR_API_KEY="llmr_sk_live_..."
export BASE="https://videorouter.sh/api/v1"

Keys can carry a monthly spend cap and a model allow-list. For experiments, create a key with a small cap. Sign up if you do not have one yet.

Step 1: create the job

curl -s "$BASE/videos" \
  -H "Authorization: Bearer $LLMR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "kling-v3.0-std",
    "prompt": "a red kite rising over a windy beach, slow upward camera tilt",
    "duration_secs": 5,
    "aspect_ratio": "16:9"
  }'

The response is a job, not a video: an id and a status of queued. The charge is taken here, at creation, based on the duration you requested, so ask for the length you intend to use. Which duration values the model accepts is a per-model fact; check the model page in the models catalog.

Capture the id in a variable so you can reuse it:

ID=$(curl -s "$BASE/videos" \
  -H "Authorization: Bearer $LLMR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"kling-v3.0-std","prompt":"a red kite rising over a windy beach","duration_secs":5}' \
  | jq -r .id)
echo "$ID"

Step 2: poll until it finishes

curl -s "$BASE/videos/$ID" -H "Authorization: Bearer $LLMR_API_KEY" | jq .status

The status moves queued, in_progress, then completed or failed. Polling is free, so a loop is fine:

while true; do
  RESP=$(curl -s "$BASE/videos/$ID" -H "Authorization: Bearer $LLMR_API_KEY")
  STATUS=$(echo "$RESP" | jq -r .status)
  echo "status: $STATUS"
  [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break
  sleep 5
done

In a real script, add a deadline so a stuck job cannot hold the terminal forever. A counter that exits after a few hundred iterations is enough for manual testing.

Step 3: extract the video URL

echo "$RESP" | jq -r 'if .status == "completed" then .data[0].url else .error end'

On success you get a URL; on failure the error field. Download it right away rather than keeping the link:

URL=$(echo "$RESP" | jq -r '.data[0].url')
curl -L -o kite.mp4 "$URL"

URLs from upstream hosts are delivery links, not permanent storage, so copy the file into your own bucket if you need it later.

Step 4: switch tier or pin a host

Kling v3.0 tiers are separate model ids, not a quality flag. Use kling-v3.0-std, kling-v3.0-pro or kling-v3.0-4k in the same request. The Std vs Pro vs 4K guide explains how to choose.

By default the request goes to the cheapest healthy host and falls back to others if one rejects it. To prefer a host, suffix it to the model id:

-d '{"model":"kling-v3.0-std/novita","prompt":"...","duration_secs":5}'

That is a preference, not a guarantee: if the named host errors, the platform may still try other hosts serving the same checkpoint. For a strict pin, add a provider object:

-d '{
  "model": "kling-v3.0-std",
  "prompt": "...",
  "duration_secs": 5,
  "provider": { "only": ["novita"], "allow_fallbacks": false }
}'

With allow_fallbacks set to false, a rejection comes straight back as an error with no retry. Check the model page for the hosts that currently serve each tier, since hosts change.

Step 5: read the errors

Failures at creation use an OpenAI-style envelope. Print the body and the status together while testing:

curl -s -w "\nHTTP %{http_code}\n" "$BASE/videos" \
  -H "Authorization: Bearer $LLMR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"not-a-real-model","prompt":"test"}'
StatusMeaningWhat to do
400Unknown model or fields the model rejectsFix the request. Do not retry.
401Missing, revoked or expired keyCheck $LLMR_API_KEY.
402Spend cap reached or balance empty; the code field tells you whichRaise the cap or top up.
403Model not on this key's allow-listAdjust the key policy.
429Rate limitWait for the Retry-After header, then retry.
5xxEvery candidate host failedRetry with backoff; documented as not billed.

A job can also be accepted and later end with status failed. In that case the polling response carries the error field. A job that fails because every upstream host failed is not billed.

Two curl gotchas

Where to go next

Once the loop works by hand, move to code with the Python tutorial, add a start frame with the image-to-video guide, or look at the quickstart for the shared basics. See pricing for live per-second rates before you batch anything.

Frequently asked questions

How do I call the Kling API with curl?

POST a JSON body with model (for example kling-v3.0-std), prompt and duration_secs to https://videorouter.sh/api/v1/videos with a Bearer key, then GET /videos/{id} until the status is completed and read data[0].url.

How do I get the video URL from the response?

When status is completed, the URL is in data[0].url. With jq: jq -r '.data[0].url'. Download it promptly because it is a delivery link.

How do I pin Kling to a specific host?

Suffix the host to the model id for a soft preference, or use provider.only with allow_fallbacks set to false for a hard pin with no retries.

Does polling cost anything?

No. Polling is free; you are billed once at creation based on the requested duration.

Keep reading

Using Kling is one part of the job.

VideoRouter puts it next to dozens of other video and image models behind one API key, so you can compare providers, prices and fail over automatically. Compare providers on VideoRouter →