Developers
Create clips from your own code with a REST API. Clips made through the API cost 10% less than on the website.
The reference below is in English.
Claude, ChatGPT and other assistants can connect to Peeka without an API key. You sign in and set a daily limit. Connect an assistant
The same reference as plain text and as an OpenAPI file: api-docs.md · openapi.json · llms.txt
https://peekaai.com/api/v1Include your API key in the Authorization header:
Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxErrors are application/problem+json (RFC 9457) with code, retryable, retry_after and user_action. Send an Idempotency-Key header on create calls so a retry is never charged twice. See Errors and Idempotency.
60 requests per minute per API key. Rate limit headers are included in every response:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1744632060/api/v1/videos/api/v1/videos/batch/api/v1/videos/{id}/retry/api/v1/videos/{id}/api/v1/videos/api/v1/videos/{id}/api/v1/assets/api/v1/assets/api/v1/assets/{id}/api/v1/assets/{id}/api/v1/account/api/v1/pricing/api/v1/videosRequest body
| Field | Type | Required | Description |
|---|---|---|---|
| prompt | string | Yes* | Text description of the video (*at least one of prompt / image_urls / asset_ids required) |
| image_urls | string[] | Yes* | Public reference image URLs (up to 5). Peeka downloads and moderates each one before generating. Requires subject_consent. Prefer asset_ids. |
| subject_consent | string | With image_urls | self (the person is you or the end user you act for), permission (the person agreed) or no_real_person. See Photos of people. |
| asset_ids | int[] | No | IDs returned by POST /api/v1/assets (up to 5). Takes priority over image_urls. Use this to reuse the same reference image across multiple calls without re-uploading. |
| callback_url | string | No | HTTPS URL we POST to when the task reaches a terminal state (done/error). Body signed with HMAC-SHA256 in X-Peeka-Signature. See "Webhook callbacks" below. |
| duration | int | No | Whole seconds: 4–15 for standard models, 4–30 for cinema models (default: 5) |
| ratio | string | No | 16:9, 9:16, 1:1, 4:3, 3:4, 21:9 (default: 16:9) |
| model | string | No | Engine and resolution: standard (480p, default), standard-hd (720p), standard-fullhd (1080p), or the newest model cinema, cinema-hd, cinema-fullhd (up to 30 s, speaking characters incl. Arabic). IDs from earlier versions are still accepted. |
| resolution | string | No | Determined by the model (480p / 720p / 1080p). If you send it, it must match the model. |
Credit costs — API (10% discount applied)
| Model | Per second | 5 s | 10 s | 15 s |
|---|---|---|---|---|
Standard 480p · standard | 2 | 9 | 18 | 27 |
Standard HD 720p · standard-hd | 4 | 18 | 36 | 54 |
Standard Full HD 1080p · standard-fullhd | 10 | 45 | 90 | 135 |
Cinema 480p · cinema | 3 | 14 | 27 | 41 |
Cinema HD 720p · cinema-hd | 6 | 27 | 54 | 81 |
Cinema Full HD 1080p · cinema-fullhd | 15 | 68 | 135 | 203 |
Standard models accept 4–15 seconds, cinema models 4–30 (30 s cinema: 81 / 162 / 405 credits). Cost = credits per second × seconds × 0.9, rounded up ("Per second" is the web price before the API discount). Current prices: GET /api/pricing.
curl -X POST https://peekaai.com/api/v1/videos \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"prompt": "A cinematic shot of a mountain sunrise, 4K, golden hour",
"duration": 5,
"ratio": "16:9",
"model": "standard"
}'curl -X POST https://peekaai.com/api/v1/videos \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Camera slowly zooms in",
"image_urls": [
"https://example.com/photo1.jpg",
"https://example.com/photo2.jpg"
],
"subject_consent": "self",
"duration": 5,
"ratio": "16:9"
}'const response = await fetch('https://peekaai.com/api/v1/videos', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PEEKA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: 'A cinematic shot of a mountain sunrise',
duration: 5,
ratio: '16:9',
}),
});
const { id, poll_url } = await response.json();import requests
resp = requests.post(
'https://peekaai.com/api/v1/videos',
headers={
'Authorization': f'Bearer {PEEKA_API_KEY}',
'Content-Type': 'application/json',
},
json={
'prompt': 'A cinematic shot of a mountain sunrise',
'duration': 5,
'ratio': '16:9',
},
)
data = resp.json()
print(data['id'], data['poll_url'])All dates are ISO 8601 format with UTC timezone (e.g. 2026-04-14T12:00:00Z).
{
"id": 42,
"status": "pending",
"credits_used": 9,
"duration": 5,
"ratio": "16:9",
"model": "standard",
"prompt": "A cinematic shot of a mountain sunrise",
"estimated_time_seconds": 30,
"created_at": "2026-04-14T12:00:00Z",
"poll_url": "https://peekaai.com/api/v1/videos/42"
}/api/v1/videos/{id}Poll this endpoint every 5–10 seconds until status becomes done or error. Typical generation takes about 1–2 minutes. video_url and poster_url are absolute signed links that open without a key for 7 days (call again for a fresh link); they stop working when the clip is deleted.
curl https://peekaai.com/api/v1/videos/42 \
-H "Authorization: Bearer pk_live_YOUR_KEY"{
"id": 42,
"status": "done",
"prompt": "A cinematic shot of a mountain sunrise",
"video_url": "https://peekaai.com/api/v1/media/42.mp4?exp=1790000000&sig=…",
"poster_url": "https://peekaai.com/api/v1/media/42.jpg?exp=1790000000&sig=…",
"share_page_url": null,
"credits_used": 9,
"refunded": false,
"model": "standard",
"duration": 5,
"ratio": "16:9",
"progress": null,
"error": null,
"error_code": null,
"created_at": "2026-04-14T12:00:00Z",
"completed_at": "2026-04-14T12:01:28Z",
"poll_url": "https://peekaai.com/api/v1/videos/42"
}async function waitForVideo(apiKey, videoId) {
const url = `https://peekaai.com/api/v1/videos/${videoId}`;
const headers = { 'Authorization': `Bearer ${apiKey}` };
while (true) {
const res = await fetch(url, { headers });
const data = await res.json();
if (data.status === 'done') return data.video_url;
if (data.status === 'error') throw new Error(data.error || 'Generation failed');
// poll every 5 seconds
await new Promise(r => setTimeout(r, 5000));
}
}Upload a reference image once, then reuse the returned asset_id on every /api/v1/videos call. Avoids re-uploading + re-moderating the same image and unlocks Peeka's native asset registry (required for real-person inputs to pass our content moderation). Same-image SHA-256 dedupe is automatic — uploading the same file twice returns the existing asset_id.
/api/v1/assetsmultipart/form-data · field name: file · jpg/png/webp/gif/heic, ≤20 MB · 0 creditsAsset moderation can take 5–10 seconds. 422 means the content was rejected — check reason_code.
curl -X POST https://peekaai.com/api/v1/assets \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-F "file=@/path/to/photo.jpg"{
"id": 1234,
"asset_uri": "asset://asset-20260517123045-a8b3",
"public_url": "https://peekaai.com/uploads/42/1715938245_a1b2c3d4.jpg",
"name": "photo.jpg",
"file_size": 184320,
"mime_type": "image/jpeg",
"trust_level": "trusted",
"deduped": false,
"created_at": "2026-05-17T12:30:45Z"
}curl -X POST https://peekaai.com/api/v1/videos \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Smile and wave at camera",
"asset_ids": [1234],
"duration": 5
}'Tip: set default assets on your API key in the Account → API tab — calls that omit image_urls and asset_ids automatically fall back to those defaults. Useful for batch jobs that should all share the same reference.
Pass callback_url when creating a video and we'll POST the result to that URL when the task reaches a terminal state (done or error). Avoids polling. Polling still works alongside webhooks.
X-Peeka-Signature. Secret is shown once when you create the API key — save it.X-Peeka-Delivery header — use it for idempotency.video_url in the payload opens for a signed-in browser. For a link that works anywhere, call GET /api/v1/videos/{id} and use its signed video_url.curl -X POST https://peekaai.com/api/v1/videos \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A serene lake at dawn",
"duration": 5,
"callback_url": "https://your-app.com/peeka/webhook"
}'Headers: X-Peeka-Event (video.completed / video.failed), X-Peeka-Signature, X-Peeka-Delivery
{
"event": "video.completed",
"id": 42,
"status": "done",
"prompt": "A serene lake at dawn",
"video_url": "https://peekaai.com/api/video-stream/42",
"error": null,
"credits_used": 9,
"duration": 5,
"model": "standard",
"created_at": "2026-05-17T12:30:00Z",
"completed_at": "2026-05-17T12:30:28Z",
"delivered_at": "2026-05-17T12:30:29Z"
}import crypto from 'crypto';
import express from 'express';
const app = express();
// Webhook signing secret returned by POST /api/api-keys (shown once)
const WEBHOOK_SECRET = process.env.PEEKA_WEBHOOK_SECRET;
// IMPORTANT: pass raw body to HMAC. Use express.raw() not express.json().
app.post('/peeka/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.header('X-Peeka-Signature') || '';
const expected = 'sha256=' + crypto.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('bad signature');
}
const payload = JSON.parse(req.body.toString());
console.log(payload.event, payload.id, payload.video_url);
res.status(200).send('ok'); // return 2xx to ack; non-2xx triggers retry
});import hmac, hashlib
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['PEEKA_WEBHOOK_SECRET']
@app.post('/peeka/webhook')
def webhook():
sig = request.headers.get('X-Peeka-Signature', '')
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(),
request.get_data(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(sig, expected):
return 'bad signature', 401
payload = request.get_json()
print(payload['event'], payload['id'], payload['video_url'])
return 'ok', 200 # return 2xx to ackLost your secret? Open Account → API tab and click "Rotate webhook secret" on the key.
When a task fails because of our infrastructure (queue overload, timeout, transient errors), its credits are refunded automatically and you can run it again once with the same prompt and assets. The retry is charged like a new request at the API price; if it fails too, it is refunded as well. User-side failures (rejected content, broken image URL, bad model name) are not eligible: retrying won't help, fix the input and submit a new request.
Cancelling (DELETE /api/v1/videos/{id}) refunds a clip only while it is still queued. Once the model is making it, it can no longer be stopped: you get 409 already_rendering and the clip finishes and is charged.
/api/v1/videos/{id}/retryno body · API price · max 1 retry per original taskEligible error_code values: upstream_busy, upstream_unavailable, upstream_timeout, upstream_error. Returns 400 not_retryable for everything else.
curl -X POST https://peekaai.com/api/v1/videos/42/retry \
-H "Authorization: Bearer pk_live_YOUR_KEY"{
"id": 73,
"status": "pending",
"retry_of_task_id": 42,
"credits_used": 9,
"free_retry": false,
"prompt": "A serene lake at dawn",
"model": "standard",
"duration": 5,
"ratio": "9:16",
"callback_url": "https://your-app.com/peeka/webhook",
"callback_enabled": true,
"estimated_time_seconds": 70,
"created_at": "2026-05-17T13:00:00Z",
"poll_url": "https://peekaai.com/api/v1/videos/73"
}The new task is a brand-new id (not the original). All original parameters (prompt, model, ratio, asset_ids, callback_url) are replayed from the snapshot. If the retry itself fails, you'll see cannot_retry_a_retry on a second retry attempt — that's by design (close the abuse loop).
Submit up to 5 video tasks in one request — ideal for multi-scene short films. Credit deduction is atomic: if your balance can't cover the total, nothing is created. Each task gets its own id + independent webhook delivery.
/api/v1/videos/batchmax 5 items · validation upfront · 207 on successBody: { items: [...], defaults: {...} }. Each items[i] accepts the same fields as POST /api/v1/videos (prompt, image_urls, asset_ids, duration, ratio, model, callback_url).defaults applies to any item that omits a field.
Validation first, deduction second: if any item fails validation, the response is 400 with item_index pointing to the offender, and no credits are deducted. Only after every item passes validation do we deduct the total cost (sum of per-item costs) and kick off generation.
curl -X POST https://peekaai.com/api/v1/videos/batch \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"defaults": {
"model": "standard",
"duration": 5,
"ratio": "9:16",
"asset_ids": [1234],
"callback_url": "https://your-app.com/peeka/webhook"
},
"items": [
{ "prompt": "Scene 1: she walks into the cafe" },
{ "prompt": "Scene 2: she orders coffee, smiling" },
{ "prompt": "Scene 3: she sits by the window, looking out" }
]
}'Always 207 even if all items succeed — keeps clients on one parse path.
{
"batch_id": "batch_1715958000123_a1b2c3d4",
"total_credits_used": 27,
"items_count": 3,
"items": [
{
"index": 0, "ok": true, "id": 100, "status": "pending",
"credits_used": 9, "prompt": "Scene 1: she walks into the cafe",
"model": "standard", "duration": 5, "ratio": "9:16",
"asset_source": "asset_ids", "asset_ids_used": [1234],
"callback_enabled": true, "estimated_time_seconds": 70,
"poll_url": "https://peekaai.com/api/v1/videos/100"
},
{ "index": 1, "ok": true, "id": 101, "...": "..." },
{ "index": 2, "ok": true, "id": 102, "...": "..." }
],
"created_at": "2026-05-17T13:00:00Z"
}Send Idempotency-Key (1–255 printable characters, e.g. a UUID) on POST /api/v1/videos and POST /api/v1/videos/batch. The same key with the same body within 24 hours returns the original response and is charged once. The same key with a different body returns 422 idempotency_key_reused; while the first request is still running you get 409 idempotency_in_progress. If the first request failed before a task existed (validation, moderation, 402), the key is released and can be reused.
# Same key + same body within 24 h → the original task, charged once
curl -X POST https://peekaai.com/api/v1/videos \
-H "Authorization: Bearer pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234-clip-1" \
-d '{"prompt": "A lantern floats over the dunes", "duration": 5}'
# Replays answer with the same body and the header
Idempotent-Replayed: trueimage_urls need subject_consent: self, permission or no_real_person. Without it: 422 consent_required, nothing charged.422 photo_rejected; if the check takes longer than about 40 seconds you get 503 moderation_pending with the asset_ids to use later.POST /api/v1/assets (add subject_consent as a form field) and passing asset_ids. asset:// URIs are only accepted for your own moderated assets.All errors are RFC 9457 problem details. Use code for programmatic handling; retryable and retry_after tell you whether and when to retry; user_action.url is for a person (top up, confirm consent). The old error string is still included.
HTTP/1.1 402 Payment Required
Content-Type: application/problem+json
{
"type": "https://peekaai.com/api-docs#error-insufficient_credits",
"title": "Not enough credits",
"status": 402,
"detail": "Insufficient credits",
"code": "insufficient_credits",
"error": "Insufficient credits",
"retryable": false,
"retry_after": null,
"user_action": { "kind": "top_up", "url": "https://peekaai.com/recharge?src=agent" },
"required": 9,
"balance": 4,
"trace_id": "8f2c1a9b"
}| HTTP | code | Meaning |
|---|---|---|
| 400 | missing_input | Neither prompt nor image_urls / asset_ids provided |
| 400 | invalid_model / invalid_ratio / invalid_duration / invalid_resolution / resolution_model_mismatch | Unknown or inconsistent parameter; the body lists the allowed values |
| 400 | invalid_image_url / invalid_callback_url / invalid_asset_id / too_many_images | URL failed the safety check, asset not yours / not moderated, or more than 5 images |
| 400 | invalid_json / batch_too_large / invalid_idempotency_key | Malformed request |
| 401 | invalid_auth_header / invalid_api_key | Missing, revoked, or invalid API key |
| 402 | insufficient_credits | Balance too low (required / balance included); user_action links to top-up |
| 404 | not_found | Video / asset ID does not exist or belongs to another account |
| 409 | idempotency_in_progress | The first request with this Idempotency-Key is still running; retry after retry_after |
| 409 | media_not_ready | Signed link: clip finished but still being saved; retry in a few seconds |
| 409 | already_rendering | Cancel too late: the clip is already being made; it will finish and is charged |
| 409 | already_retried / retry_in_progress | This clip was already run again, or a retry is being started |
| 410 | link_expired | Signed link older than 7 days; fetch the clip again for a fresh one |
| 422 | consent_required | image_urls sent without subject_consent; nothing was charged |
| 422 | photo_rejected / moderation_rejected | The photo failed Peeka moderation (reason_code included); nothing was charged |
| 422 | image_fetch_failed | We could not download an image_url; upload it with POST /api/v1/assets instead |
| 422 | idempotency_key_reused | Same Idempotency-Key with a different body; use a new key |
| 429 | rate_limit_exceeded | 60 requests/min per key; wait retry_after seconds (Retry-After header) |
| 503 | moderation_pending / upstream_busy / no_session_available / generation_paused | Temporary; nothing was charged. Retry after retry_after seconds |
| 500 | task_create_failed / internal_error | Our side; credits refunded. Retry with the same Idempotency-Key and quote trace_id to support |
error_code)When a task ends in status: "error", the response includes error_code for programmatic handling. Same codes appear in webhook payloads.
| error_code | Meaning + retry guidance |
|---|---|
| image_fetch_failed | Our generation provider could not download your image_urls. Common cause: image host blocks server-side fetchers (e.g. catbox.moe, some Cloudflare configs). Fix: upload via POST /api/v1/assets and use asset_ids instead, or rehost on a CDN that allows server-side fetches. |
| content_rejected | Prompt or image rejected by content moderation. Retrying won't help — adjust the input. |
| upstream_busy | Rate limit / queue overload on the generation backend. Retry with exponential backoff. |
| upstream_unavailable | 5xx from the generation backend. Retry after 1–2 minutes. |
| upstream_timeout | Generation exceeded our polling timeout (10 min). Credits already refunded — retry. |
| upstream_auth_failed | Our backend credentials broke — please email support, do not retry. |
| upstream_bad_request / upstream_error | Other upstream rejection — see error field for details. |
Claude, ChatGPT and other MCP clients don't need an API key. They connect to the remote MCP server at https://peekaai.com/mcp (Streamable HTTP; OAuth 2.1 sign-in with the user's Peeka account). The user sets a daily credit limit per assistant (30 by default) and can disconnect it at any time. Assistants pay app prices, must pass max_credits on every paid call, and can only use photos the user uploaded and confirmed on Peeka. It is live. GET /api/agent/status shows whether it is currently enabled. Setup steps: /connect.
claude mcp add --transport http peeka https://peekaai.com/mcpUse the Peeka API (https://peekaai.com/api-docs.md, OpenAPI: https://peekaai.com/openapi.json).
Auth: Authorization: Bearer $PEEKA_API_KEY. Base URL https://peekaai.com/api/v1.
Create clips with POST /videos and always send an Idempotency-Key header.
For photos of people, upload with POST /assets (subject_consent) and pass asset_ids.
Poll GET /videos/{id} every 10 s until status is done or error; use the signed video_url.
Errors are RFC 9457 problem+json: respect retryable and retry_after, and show user_action.url to the user.Questions, higher rate limits or bug reports: email us.
contact@peekaai.com