Search

Search HiAPI models, tools, and resources.

Back to blog
Tutorial

How to Use the kling-3.0-omni/transformation API: Video Restyle with curl and Python

hiapitutorialvideorecipe

kling-3.0-omni/transformation is Kling 3.0 Omni's video-to-video model: you hand it one source clip plus a text prompt, and it rewrites the look of that clip — scene, style, subject — while keeping the original motion and timing. On hiapi you call it through the same unified async endpoint as every other generation model: POST /v1/tasks. This guide gives you the live-verified parameter table, a copy-paste curl request, a complete Python client that polls and downloads the result, and the production patterns (callbacks, idempotency, error handling) you need before wiring it into a real app.

Every field name, enum value, and response shape below was checked against the live API.

What you'll build + prerequisites

A tiny program that submits a restyle task, waits for it to finish, and downloads the restyled .mp4. You need two things:

  1. An hiapi API key. Create one in your hiapi dashboard — it looks like sk-.... Every request uses Authorization: Bearer sk-<your-key>.
  2. One publicly reachable source video URL. The model downloads your clip server-side, so it must be a public http(s) URL (your own CDN, R2/S3 with a public object, etc.), not a local file path. Exactly one URL is allowed.

The model and its parameters

The request envelope is {"model": ..., "input": {...}}. The input schema is strict — only these three fields are accepted, and anything extra is rejected with a 400.

FieldTypeRequiredNotes
promptstringyesDescribes the target style/scene/subject. This is also where you dial restyle strength — there is no numeric strength/fidelity param.
video_urlsarrayyesExactly one public source video URL (minItems 1, maxItems 1).
resolutionstring enumyesOne of 720p, 1080p, 4k.

A few things that trip people up:

  • There is no duration parameter. Output length and pacing follow your source clip's motion — the model restyles what's there, it doesn't re-time it.
  • The model id carries -omni in the id itself: kling-3.0-omni/transformation (some other Omni endpoints use kling-3.0/...). Use the bare id as the model value.
  • Restyle strength is prompt-driven. Say "subtle repaint, keep facial features and lighting" for a light touch, or "fully re-imagine as a 2D cel-shaded cartoon" for a heavy one.

Minimal working example (curl)

# 1) Create the restyle task
curl -s https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer sk-<your-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "kling-3.0-omni/transformation",
    "input": {
      "prompt": "Restyle the scene as a hand-drawn watercolor animation with soft paper texture and warm dusk light; keep the original camera motion and subject.",
      "video_urls": ["https://your-cdn.example.com/source-clip.mp4"],
      "resolution": "720p"
    }
  }'
# -> {"code":200,"data":{"taskId":"tk-hiapi-01..."},"message":"success"}

# 2) Poll the task until it reaches a terminal state
curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-01... \
  -H "Authorization: Bearer sk-<your-key>"
# data.status progresses: queued -> handling -> archiving -> success
# on success, data.output[0].url is the restyled .mp4

The task id you poll is data.taskId from the create response — not a top-level id. The status strings are queued / handling / archiving / success (terminal) or fail; don't assume succeeded/completed.

Complete Python client (create → poll → download)

import os
import time
import requests

API_KEY = os.environ["HIAPI_KEY"]            # your sk-... key
BASE = "https://api.hiapi.ai"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}


def create_restyle(source_video_url: str, prompt: str, resolution: str = "720p") -> str:
    body = {
        "model": "kling-3.0-omni/transformation",
        "input": {
            "prompt": prompt,
            "video_urls": [source_video_url],   # exactly one PUBLIC url
            "resolution": resolution,           # "720p" | "1080p" | "4k"
        },
    }
    r = requests.post(f"{BASE}/v1/tasks", json=body, headers=HEADERS, timeout=30)
    r.raise_for_status()
    data = r.json()
    if data.get("code") != 200:
        raise RuntimeError(f"submit rejected: {data}")
    return data["data"]["taskId"]


def wait_for_video(task_id: str, poll_s: int = 10, timeout_s: int = 900) -> str:
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        r = requests.get(f"{BASE}/v1/tasks/{task_id}", headers=HEADERS, timeout=30)
        r.raise_for_status()
        data = r.json()["data"]
        status = data["status"]
        if status == "success":
            return data["output"][0]["url"]
        if status == "fail":
            raise RuntimeError(f"task failed: {data.get('error')}")
        time.sleep(poll_s)                      # queued / handling / archiving
    raise TimeoutError(f"{task_id} did not finish within {timeout_s}s")


if __name__ == "__main__":
    task_id = create_restyle(
        source_video_url="https://your-cdn.example.com/source-clip.mp4",
        prompt=(
            "Restyle the scene as a hand-drawn watercolor animation, soft paper "
            "texture, warm dusk light; keep the original camera motion and subject."
        ),
        resolution="720p",
    )
    print("task:", task_id)

    url = wait_for_video(task_id)
    print("video url:", url)

    # Output lives on temp.hiapi.ai and expires (~7 days). Download it now.
    clip = requests.get(url, timeout=120).content
    with open("restyled.mp4", "wb") as f:
        f.write(clip)
    print("saved restyled.mp4", len(clip), "bytes")

Run it with your key in the environment:

export HIAPI_KEY=sk-<your-key>
python restyle.py

Production patterns

Use a callback instead of tight polling

For anything beyond a script, register a webhook so you don't hold a polling loop open. callback is a top-level field — a sibling of model and input, never inside input:

{
  "model": "kling-3.0-omni/transformation",
  "input": {
    "prompt": "Restyle as 2D cel-shaded cartoon, bold outlines; keep original motion.",
    "video_urls": ["https://your-cdn.example.com/source-clip.mp4"],
    "resolution": "1080p"
  },
  "callback": { "url": "https://your-app.example.com/hooks/hiapi", "when": "final" }
}
  • callback.when only accepts final (the default). It fires once on the terminal state, and both success and fail trigger it.
  • The delivered POST body is identical to the data object you'd get from GET /v1/tasks/<id> — so your handler can read status, output[].url, and error without a re-fetch.
  • Putting callback inside input fails at submit with additional properties 'callback' not allowed (the per-model input schema is strict).

Idempotency and delivery semantics

Callbacks are at least once, not exactly once — the same taskId can arrive more than once. Key your handler on taskId and make the "mark done + store the video" step idempotent. hiapi retries a non-2xx/timeout callback on a schedule of immediately → +1 min → +5 min → +20 min, up to 4 attempts; your endpoint must return HTTP 2xx to stop the retries.

Download outputs immediately

data.output[0].url points at temp.hiapi.ai with an expireAt (unix timestamp, roughly a 7-day TTL). After expiry the object is purged and the URL returns a 404 — re-polling the task returns the same URL but does not regenerate the file. Copy the bytes to your own storage as soon as the task succeeds.

Error handling

Three failure shapes you should branch on:

  • Bad request (HTTP 400) — flat envelope, no task created: {"code":400,"data":null,"error_code":"INVALID_REQUEST","message":"invalid input: resolution: value must be one of '720p', '1080p', '4k'"}
  • Auth failure (HTTP 401) — a nested envelope: {"error":{"code":"permission_denied","type":"hiapi_error", ...}}
  • Failed task — the submit returns 200 + a taskId, but a later poll shows data.status == "fail" with a data.error. Failed tasks are not charged (the reserved amount is refunded on terminal fail). An unreachable or non-downloadable source URL shows up here.

Cost

kling-3.0-omni/transformation is billed per second of output, and the rate scales with the resolution you request (4k costs more than 720p). See the live hiapi pricing page for the current per-second rate before you batch-run clips — video tasks add up far faster than image ones.

FAQ

How do I control how strongly it restyles the clip? Through the prompt, not a parameter. There is no strength, fidelity, or creativity field. Phrase a light edit as "subtle repaint, preserve faces and lighting" and a heavy edit as "fully re-imagine as <style>". Passing a numeric strength field returns a 400 additional properties not allowed.

What resolutions are supported? 720p, 1080p, or 4k. Any other value (including 2k or 1440p) is rejected with value must be one of '720p', '1080p', '4k'.

Can I send more than one source video? No. video_urls takes exactly one URL. For multi-clip or reference-image workflows you'd use a different Kling endpoint (e.g. reference-to-video), not transformation.

How long will the output be? The model keeps the source clip's motion and timing, so the output tracks the length of the clip you upload — there's no duration control on this endpoint.

Does it keep the original camera movement and action? Yes — that's the point of video-to-video: motion is preserved, only the visual style/scene is rewritten. Reinforce it in the prompt ("keep the original camera motion and subject") for the most faithful motion.

Should I poll or use a callback? Poll for quick scripts and one-offs; use a callback with when: "final" for production so you're not holding a loop open. Polling is always available as the ground-truth fallback even when you also register a callback.

I can't find this model in /v1/models — is it real? Yes. kling-3.0-omni/transformation is callable on /v1/tasks even though it may not appear in the /v1/models listing yet; a submit returns normal schema validation rather than MODEL_UNAVAILABLE. Use the model id exactly as written.

Why am I getting additional properties ... not allowed? The input schema accepts only prompt, video_urls, and resolution. Remove any extra fields (duration, strength, etc.), and make sure callback is at the top level of the body, not inside input.

Generate it with HiAPI

Choose a model, enter your prompt, and see the result.

HiAPI Blog

Related articles

HiAPI

Generate it with HiAPI

Start generating