Advanced lip-sync for videos. Identify faces then sync them with custom audio tracks.
Add Authorization header with Bearer token to all requests:
Authorization: Bearer YOUR_API_KEY
| Model | API Name | Features | Price (up to 60% off) |
|---|---|---|---|
| Face Recognition | kling-identify-face | Detect faces in video, returns sessionId + faceId | $0.0100/call |
| Lip-Sync Video | kling-lip-sync-video | Lip-sync a video using sessionId + faceId + audio | $0.0650/5s |
| Operation | Price |
|---|---|
| Face Recognition (kling-identify-face) | $0.0100/call |
| Lip-Sync Video (kling-lip-sync-video) | $0.0650/5s |
/api/v1/video/generationsCreate a face recognition or lip sync task. Set model to "kling-identify-face" or "kling-lip-sync-video".
/api/v1/video/generations?task_id=xxxQuery task status and retrieve results
Lip sync requires two API calls. First, submit your video to "kling-identify-face" — this returns a session_id and a list of detected faces with their face_id values. Then, submit a second request to "kling-lip-sync-video" with the session_id, the face_id(s) you want to sync, and the corresponding audio source.
Submit a video to identify faces. Returns a sessionId and faceId in resultJson, used by Step 2. $0.01 per call.
Use the sessionId and faceId from Step 1 to lip-sync the video with an audio track (audioId from kling-lip-sync-tts, or a direct audioUrl).
Synthesize speech from text with a preset voice — used to drive lip-sync. Returns an audio result.
# Step 1: Identify faces in the video
curl -X POST https://apimodels.app/api/v1/video/generations \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kling-identify-face",
"video_url": "https://example.com/video.mp4"
}'{
"code": 200,
"msg": "success",
"data": {
"taskId": "clxxx...",
"state": "pending",
"model": "kling-identify-face"
}
}{
"code": 200,
"msg": "success",
"data": {
"taskId": "clxxx...",
"state": "completed",
"model": "kling-identify-face",
"resultUrls": [],
"resultJson": "{ \"resultUrls\": [], \"sessionId\": \"891032747618619461\", \"faceId\": \"0\", \"faces\": [{ \"face_id\": \"0\", \"start_time\": 0, \"end_time\": 13500 }] }"
}
}JSON.parse(data.resultJson) → use sessionId and faceId for kling-lip-sync-video. faces[] also carries the start_time / end_time window per face.
{
"code": 200,
"msg": "success",
"data": {
"taskId": "clxxx...",
"state": "completed",
"model": "kling-lip-sync-video",
"resultUrls": ["https://...lip_synced_video.mp4"]
}
}{
"code": 200,
"msg": "success",
"data": {
"taskId": "clxxx...",
"state": "failed",
"failMsg": "Face detection failed: no faces found"
}
}Pass callback_url in the create request. When the task reaches the completed or failed terminal state, our server sends a single HTTP POST to that URL with Content-Type: application/json (no signing header). Delivery is retried up to 3 times (exponential backoff 1s/2s/4s, 10s per attempt); if still unsuccessful, a background job keeps retrying for up to 30 minutes until your endpoint returns 2xx.
POST {your callback_url}
Content-Type: application/json
{
"code": 200,
"msg": "success",
"data": {
"taskId": "clxxx...",
"model": "rh-lip-sync-video/kling-lip-sync-video",
"state": "completed" | "failed",
"param": "<JSON string>", // request params, JSON.parse once
"resultJson": "<JSON string> | null", // result object, JSON.parse once
"failCode": null | "CONTENT_MODERATION | INVALID_INPUT | INSUFFICIENT_BALANCE | UPSTREAM_BUSY | UPSTREAM_FAILED | TIMEOUT | INTERNAL_ERROR | OTHER",
"failMsg": null | "string",
"retryable": true | false, // present when state=failed: safe to retry/fallback
"costTime": 12345, // duration in ms
"completeTime": 1705123500000, // ms epoch
"createTime": 1705123450000 // ms epoch
}
}Note: data.param and data.resultJson are both JSON strings — call JSON.parse once on each.
{
"resultUrls": ["https://r2.apimodels.app/videos/lip_synced.mp4"],
"videoDuration": 8 // optional, seconds
}Lip-sync produces a video file. resultUrls is always present (length 1 in success, [] on failure). videoDuration may be present on some upstream branches. When state=failed, resultJson is typically null or {"resultUrls":[]}.
app.post('/webhook/kling-lip-sync', express.json(), (req, res) => {
const { taskId, state, resultJson, failMsg } = req.body.data
if (state === 'completed') {
const r = JSON.parse(resultJson)
console.log('lip-sync ready', taskId, r.resultUrls[0])
} else {
console.warn('lip-sync failed', taskId, failMsg)
}
res.status(200).end() // must be 2xx, otherwise we retry
})pendingTask queued, waiting to processprocessingTask is being executedcompletedTask completed successfullyfailedTask failed400Bad Request - Missing or invalid parameters401Unauthorized - Invalid API key402Payment Required - Insufficient credits404Not Found - Task ID not found500Internal Server Error