How to Call the Kling API (Python + curl, Std/Pro/4K)
Kling v3.0 goes through the same /v1/videos endpoint as every other video model VideoRouter routes — one request shape, async and job-based, regardless of which of its three tiers (Std, Pro, 4K) or which host you're calling. What's worth its own guide isn't the request shape, it's the tier and provider structure: unlike a model with resolution parameters, Kling's tiers are separate model IDs, and its hosting market is narrow enough (effectively two independently-priced hosts, plus one resale channel) that provider pinning is a simpler decision here than for a model with a dozen hosts to weigh.
Text-to-video
import time
import requests
resp = requests.post(
"https://videorouter.sh/api/v1/videos",
headers={"Authorization": "Bearer llmr_sk_live_...", "Content-Type": "application/json"},
json={
"model": "kling-v3.0-std",
"prompt": "a kite surfer carving across a turquoise bay",
"duration_secs": 5,
},
)
job = resp.json()
while job["status"] not in ("completed", "failed"):
time.sleep(5)
job = requests.get(
f"https://videorouter.sh/api/v1/videos/{job['id']}",
headers={"Authorization": "Bearer llmr_sk_live_..."},
).json()
if job["status"] == "completed":
video_url = job["data"][0]["url"]
else:
raise RuntimeError(job["error"])
Equivalent curl:
job=$(curl -s https://videorouter.sh/api/v1/videos \
-H "Authorization: Bearer llmr_sk_..." \
-H "Content-Type: application/json" \
-d '{"model": "kling-v3.0-std", "prompt": "a kite surfer carving across a turquoise bay", "duration_secs": 5}')
id=$(echo "$job" | jq -r .id)
status=$(echo "$job" | jq -r .status)
while [ "$status" != "completed" ] && [ "$status" != "failed" ]; do
sleep 5
job=$(curl -s "https://videorouter.sh/api/v1/videos/$id" -H "Authorization: Bearer llmr_sk_...")
status=$(echo "$job" | jq -r .status)
done
echo "$job" | jq -r 'if .status == "completed" then .data[0].url else .error end'
Switching tiers: three model IDs, not one parameter
Unlike models where resolution is a resolution field on a single model ID, Kling's Std, Pro, and 4K tiers are three distinct model IDs — kling-v3.0-std, kling-v3.0-pro, kling-v3.0-4k. There's no tier or quality parameter that switches between them; you change the model field itself:
job = requests.post(
"https://videorouter.sh/api/v1/videos",
headers={"Authorization": "Bearer llmr_sk_live_...", "Content-Type": "application/json"},
json={
"model": "kling-v3.0-pro", # or kling-v3.0-std, or kling-v3.0-4k
"prompt": "a kite surfer carving across a turquoise bay",
"duration_secs": 5,
},
).json()
This matters operationally: a tiered pipeline that generates Std drafts and promotes approved ones to Pro or 4K is a model string swap in your own routing code, not three separate integrations against three separate APIs.
Pinning to a specific provider
With no provider field, requests route automatically toward the cheaper healthy hosts. Kling's market is narrow enough that pinning is straightforward — suffix the model string with a host name:
job = requests.post(
"https://videorouter.sh/api/v1/videos",
headers={"Authorization": "Bearer llmr_sk_live_...", "Content-Type": "application/json"},
json={
"model": "kling-v3.0-std/novita", # or /wavespeed — priced identically to Novita
"prompt": "a kite surfer carving across a turquoise bay",
"duration_secs": 5,
},
).json()
The confirmed suffixes for Kling v3.0 are novita, wavespeed, and openrouter (Std and Pro only — OpenRouter doesn't currently list 4K). Novita and WaveSpeedAI price Std, Pro, and 4K identically to each other, so pinning between those two is purely a redundancy or account-relationship decision, not a price one — see Novita vs WaveSpeedAI for Kling API Access. OpenRouter's resale channel carries a consistent 67% markup over Novita/WaveSpeedAI on both Std and Pro — see Kling v3.0 API Pricing for the full table — so pinning to openrouter specifically is rarely the right default unless you have a separate reason (an existing OpenRouter billing relationship, for instance) to prefer it.
Handling errors correctly
Every VideoRouter error follows the same envelope regardless of underlying model or host — {"error": {"message", "type", "code"}}, the same shape as the OpenAI API:
| Status | type / code | What it means |
|---|---|---|
| 400 | invalid_request_error |
Missing prompt, or a model id not in the catalog (a typo in kling-v3.0-std won't fall back to a close match) |
| 401 | invalid_api_key |
Key missing, malformed, revoked, or expired |
| 402 | spend_cap_exceeded / insufficient_credits |
Monthly cap hit, or prepaid balance ≤ $0 |
| 403 | model_not_allowed |
The requested Kling tier isn't in this key's model_allowlist |
| 429 | rpm_limit / tpm_limit |
Rate limit exceeded — Retry-After tells you how long to wait |
| 500 / 502 / 503 / 504 | upstream_error |
Every candidate host failed — not billed |
Given Kling has only two independently-priced hosts (Novita, WaveSpeedAI) plus one resale channel, a genuine upstream_error on an unpinned request means both primary hosts failed — a stronger signal than the same error on a model with a dozen hosts to fail over across. If you've pinned to a single host via the provider suffix, this is the exact tradeoff you accepted: no automatic retry against the second host.
import time
import requests
API_BASE = "https://videorouter.sh/api/v1"
API_KEY = "llmr_sk_live_..."
def generate_kling_video(prompt, tier="std", provider=None, **kwargs):
model = f"kling-v3.0-{tier}" + (f"/{provider}" if provider else "")
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
payload = {"model": model, "prompt": prompt, **kwargs}
for attempt in range(3):
resp = requests.post(f"{API_BASE}/videos", headers=headers, json=payload)
if resp.status_code == 429:
time.sleep(int(resp.headers.get("Retry-After", 5)))
continue
resp.raise_for_status()
job = resp.json()
break
else:
raise RuntimeError("exceeded retry budget on job creation")
deadline = time.time() + 600
while job["status"] not in ("completed", "failed"):
if time.time() > deadline:
raise TimeoutError(f"job {job['id']} did not complete within 10 minutes")
time.sleep(5)
job = requests.get(f"{API_BASE}/videos/{job['id']}", headers=headers).json()
if job["status"] == "failed":
raise RuntimeError(f"generation failed: {job['error']}")
return job["data"][0]["url"]
# usage
url = generate_kling_video("a kite surfer carving across a turquoise bay", tier="pro", duration_secs=5)
Common mistakes
Assuming 4K is available everywhere Std and Pro are. OpenRouter doesn't carry kling-v3.0-4k at all — if your pipeline needs 4K specifically and you've defaulted to the OpenRouter suffix elsewhere in your code for consistency, that request will 400 rather than silently downgrading to a lower tier.
Treating Std, Pro, and 4K as a resolution parameter instead of separate model IDs. Passing a resolution field to kling-v3.0-std doesn't upgrade you to Pro or 4K quality — Kling's tiers are gated entirely by which model ID you call, unlike Seedance or Wan 3.0 where resolution is a parameter on one model ID.
Assuming Novita and WaveSpeedAI differ on something other than price. They're priced identically at every tier, so if you're choosing between the two suffixes based on a price comparison, there isn't one to make — the deciding factor is whichever host you already have better latency or account history with.
See /docs/video-generation for the complete request reference, and Kling v3.0 API Pricing: Std vs Pro vs 4K Compared for the full per-tier, per-host pricing table this guide's provider suffixes map to.
Frequently asked questions
Can I request Kling v3.0 without specifying a tier? No — there's no bare kling-v3.0 model id; you must pick kling-v3.0-std, kling-v3.0-pro, or kling-v3.0-4k explicitly, since VideoRouter has no basis to guess which tier's price and quality tradeoff you want.
Does pinning to Novita vs WaveSpeedAI change the price I'm billed? No — see Novita vs WaveSpeedAI for Kling API Access for the full tie-breakdown across all three tiers. Pinning between these two is a redundancy/latency decision, not a cost one.
What happens if I pin to openrouter for the 4K tier? You'll get a 400 invalid_request_error — OpenRouter simply doesn't list Kling 4K, so there's no valid combination to route to.
Is there a Kling image-to-video mode? Check /docs/video-generation for the current parameter shape — Kling's image-to-video support varies by host, and this is exactly the kind of capability detail worth verifying against live docs rather than a pricing-focused article like this one.
How do I know which host actually served a completed job if I didn't pin? The completed job object includes provider attribution — check the exact field name in /docs/api-reference-video rather than assuming based on which suffix (if any) you used in the request.