Generate talking videos programmatically using the LovelyBots API. This skill lets you queue a video from a script and source image, poll until it's ready, and return the final video URL.
Get your API key at: https://lovelybots.com/developer API base URL for bots: https://api.lovelybots.com/api
image field)Set your LovelyBots API key as an environment variable:
bash
export LOVELYBOTS_API_KEY=your_api_key_here
Or add it to your openclaw.json:
json
{
"skills": {
"entries": {
"tiktok-video-maker": {
"env": {
"LOVELYBOTS_API_KEY": "your_api_key_here"
}
}
}
}
}
https://api.lovelybots.com/api. Do not use the web app host for API requests.Authorization: Bearer $LOVELYBOTS_API_KEY on every API request.video.id and voice_id as UUID strings. Never assume numeric IDs.GET /api/videos/:id until terminal status ( completed or failed) with timeout and retry guards.http/https URLs. Localhost/private-network URLs are blocked.https://lovelybots.com (dashboard/login/docs), and keep automation calls on api.lovelybots.com.Best quality input recommendation:
9:16 orientation (for example 1080x1920).bash
curl -X POST https://api.lovelybots.com/api/create \
-H "Authorization: Bearer $LOVELYBOTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"script": "Welcome to our summer sale. Use code SAVE20 for 20% off everything this week only.",
"image": "https://example.com/your-image-1080x1920.jpg",
"public": false,
"action_prompt": "Subject smiles warmly and waves at the camera.",
"camera_prompt": "Medium closeup, static camera, cinematic lighting, 4k."
}'
Response:
json
{
"id": "b6f9a32d-3c53-4a6c-9d8c-2f0f7a1b4cde",
"status": "queued",
"credits_remaining": 1,
"share_url": "https://lovelybots.com/videos/b6f9a32d-3c53-4a6c-9d8c-2f0f7a1b4cde/share/abc123token"
}
bash
curl "https://api.lovelybots.com/api/videos/$VIDEO_ID" \
-H "Authorization: Bearer $LOVELYBOTS_API_KEY"
Response when processing:
json
{
"id": "b6f9a32d-3c53-4a6c-9d8c-2f0f7a1b4cde",
"status": "processing",
"credits_remaining": 9,
"share_url": "https://lovelybots.com/videos/b6f9a32d-3c53-4a6c-9d8c-2f0f7a1b4cde/share/abc123token"
}
When status is queued or processing, immediately report progress to the user (including share_url) before continuing to poll.
Template:
text
Status Update:
Job ID: <id>
Status: <status>
Credits Remaining: <credits_remaining>
Share URL: <share_url>
Response when complete:
json
{
"id": "b6f9a32d-3c53-4a6c-9d8c-2f0f7a1b4cde",
"status": "completed",
"video_url": "https://lovelybots.com/videos/b6f9a32d-3c53-4a6c-9d8c-2f0f7a1b4cde.mp4",
"share_url": "https://lovelybots.com/videos/b6f9a32d-3c53-4a6c-9d8c-2f0f7a1b4cde/share/abc123token",
"credits_remaining": 9
}
Response if failed:
json
{
"id": "b6f9a32d-3c53-4a6c-9d8c-2f0f7a1b4cde",
"status": "failed",
"error": "Image could not be processed",
"credits_remaining": 10,
"share_url": "https://lovelybots.com/videos/b6f9a32d-3c53-4a6c-9d8c-2f0f7a1b4cde/share/abc123token"
}
Once status is completed, return video_url to the user. The video is ready to download or share.
Poll every 5–10 seconds. Most videos complete within 60–120 seconds. If status is still processing after 5 minutes, surface an error to the user.
Suggested polling loop (bash):
bash
VIDEO_ID="b6f9a32d-3c53-4a6c-9d8c-2f0f7a1b4cde"
POLL_INTERVAL_SECONDS=8
MAX_WAIT_SECONDS=300
START_TS=$(date +%s)
HEADERS_FILE=$(mktemp)
trap 'rm -f "$HEADERS_FILE"' EXIT
extract_json_field() {
local key="$1"
if command -v jq >/dev/null 2>&1; then
jq -r --arg key "$key" '.[$key] // empty'
return
fi
if command -v python3 >/dev/null 2>&1; then
python3 -c 'import json,sys; key=sys.argv[1]; data=json.load(sys.stdin); value=data.get(key, ""); print("" if value is None else value)' "$key"
return
fi
echo "Install jq or python3 to parse API responses in this polling loop." >&2
return 127
}
while true; do
NOW_TS=$(date +%s)
if [ $((NOW_TS - START_TS)) -ge "$MAX_WAIT_SECONDS" ]; then
echo "Timed out after ${MAX_WAIT_SECONDS}s waiting for video completion." >&2
break
fi
HTTP_RESPONSE=$(curl -sS --connect-timeout 10 --max-time 30 \
-D "$HEADERS_FILE" \
-w $'\n%{http_code}' "https://api.lovelybots.com/api/videos/$VIDEO_ID" \
-H "Authorization: Bearer $LOVELYBOTS_API_KEY")
CURL_EXIT=$?
if [ "$CURL_EXIT" -ne 0 ]; then
echo "Polling request failed (curl exit $CURL_EXIT). Retrying..." >&2
sleep "$POLL_INTERVAL_SECONDS"
continue
fi
HTTP_CODE=$(printf '%s\n' "$HTTP_RESPONSE" | tail -n 1)
RESPONSE=$(printf '%s\n' "$HTTP_RESPONSE" | sed '$d')
if [ "$HTTP_CODE" = "429" ] || [ "$HTTP_CODE" -ge 500 ]; then
RETRY_AFTER=$(awk 'tolower($1)=="retry-after:" {print $2}' "$HEADERS_FILE" | tr -d '\r' | tail -n 1)
if printf '%s' "$RETRY_AFTER" | grep -Eq '^[0-9]+$'; then
WAIT_SECONDS="$RETRY_AFTER"
else
WAIT_SECONDS="$POLL_INTERVAL_SECONDS"
fi
echo "Transient HTTP $HTTP_CODE while polling. Retrying in ${WAIT_SECONDS}s..." >&2
sleep "$WAIT_SECONDS"
continue
fi
if printf '%s' "$HTTP_CODE" | grep -Eq '^[0-9]{3}$' && [ "$HTTP_CODE" -ge 400 ]; then
API_ERROR=$(printf '%s\n' "$RESPONSE" | extract_json_field error 2>/dev/null || true)
echo "Polling failed with HTTP $HTTP_CODE${API_ERROR:+: $API_ERROR}" >&2
break
fi
STATUS=$(printf '%s\n' "$RESPONSE" | extract_json_field status) || break
if [ -z "$STATUS" ]; then
API_ERROR=$(printf '%s\n' "$RESPONSE" | extract_json_field error 2>/dev/null || true)
if [ -n "$API_ERROR" ]; then
echo "API error: $API_ERROR" >&2
else
echo "Unexpected API response (missing status)." >&2
fi
break
fi
if [ "$STATUS" = "completed" ]; then
printf '%s\n' "$RESPONSE" | extract_json_field video_url
break
elif [ "$STATUS" = "failed" ]; then
API_ERROR=$(printf '%s\n' "$RESPONSE" | extract_json_field error 2>/dev/null || true)
echo "Video generation failed${API_ERROR:+: $API_ERROR}" >&2
break
fi
sleep "$POLL_INTERVAL_SECONDS"
done
MethodPathDescriptionPOST/api/createSubmit a video generation jobPOST/api/videosAlias for /api/createGET/api/videos/:idGet job status and video URLGET/api/voicesList available voices (filter by gender/age)
FieldTypeRequiredDescriptionscriptstring✓The spoken text for the videoimagefile or string✓Unified image input. Can be multipart file upload, http/https URL, or base64/data URLpublicbooleanWhether the video appears in the public feed (default: true)genderstringmale or female — skips AI detection, speeds up responseagestringyoung_adult, adult, mature, middle_aged, or olderaction_promptstringOptional action/performance guidancecamera_promptstringOptional camera/framing guidancevoice_idstring (UUID)Specific voice ID from GET /api/voices — skips AI detection and auto-selection entirely
Image guidance:
9:16 orientation (recommended 1080x1920).image is a URL, it must be publicly reachable ( localhost and private-network hosts are blocked).FieldTypeDescriptionvoices[].idstring (UUID)Use as voice_id in create requestsvoices[].namestringVoice display namevoices[].genderstringmale or femalevoices[].agestringyoung_adult, adult, mature, middle_aged, or older
FieldTypeDescriptionidstring (UUID)Job IDstatusstringqueued / processing / completed / failedvideo_urlstringDownload URL (only when completed)share_urlstringPublic shareable page URL (always present)credits_remainingintegerVideos remaining in your plan this montherrorstringError message (only when failed)
ProblemLikely CauseFix401 Invalid or expired API tokenMissing/incorrect Bearer tokenRegenerate token at https://lovelybots.com/developer and send Authorization: Bearer $LOVELYBOTS_API_KEY``403 errors ( Please select a plan, monthly limit, or IP not allowed)Plan/usage/IP restrictionsEnsure subscription is active, check monthly limit, and verify token IP allowlist settings404 Video not foundWrong video.id or token user mismatchUse the UUID returned by create response, and poll using the same API token owner422 image is required or image URL errorsMissing image or blocked URLSend image as upload/URL/base64. If URL, it must be public http/https (no localhost/private network)429 Rate limit exceededToo many requestsIncrease polling interval, add jitter/backoff, retry latercf-mitigated: challenge header appearsRequest sent to proxied hostUse https://api.lovelybots.com/api/... (DNS-only API host), not https://lovelybots.com/api/...Poll loop times outLong render time or transient API/network issueRaise MAX_WAIT_SECONDS, inspect API error payload, and retry with same video.id