Developers

API documentation

Create clips from your own code with a REST API. Clips made through the API cost 10% less than on the website.

Quick start

  1. API keys unlock once your balance reaches a minimum amount. You can see the exact amount in Account → API keys.
  2. Create a key and copy it (it's shown only once): Account → API keys
  3. Send your first request. The examples below show how.

The reference below is in English.

Using an AI assistant?

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

Base URL

https://peekaai.com/api/v1

Authentication

Include your API key in the Authorization header:

Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx

Errors and retries

Errors 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.

Rate limits

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

Endpoints

POST/api/v1/videos
POST/api/v1/videos/batch
POST/api/v1/videos/{id}/retry
GET/api/v1/videos/{id}
GET/api/v1/videos
DELETE/api/v1/videos/{id}
POST/api/v1/assets
GET/api/v1/assets
GET/api/v1/assets/{id}
DELETE/api/v1/assets/{id}
GET/api/v1/account
GET/api/v1/pricing

Create a video

POST/api/v1/videos

Request body

FieldTypeRequiredDescription
promptstringYes*Text description of the video (*at least one of prompt / image_urls / asset_ids required)
image_urlsstring[]Yes*Public reference image URLs (up to 5). Peeka downloads and moderates each one before generating. Requires subject_consent. Prefer asset_ids.
subject_consentstringWith image_urlsself (the person is you or the end user you act for), permission (the person agreed) or no_real_person. See Photos of people.
asset_idsint[]NoIDs 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_urlstringNoHTTPS 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.
durationintNoWhole seconds: 4–15 for standard models, 4–30 for cinema models (default: 5)
ratiostringNo16:9, 9:16, 1:1, 4:3, 3:4, 21:9 (default: 16:9)
modelstringNoEngine 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.
resolutionstringNoDetermined by the model (480p / 720p / 1080p). If you send it, it must match the model.

Credit costs — API (10% discount applied)

ModelPer second5 s10 s15 s
Standard 480p · standard291827
Standard HD 720p · standard-hd4183654
Standard Full HD 1080p · standard-fullhd104590135
Cinema 480p · cinema3142741
Cinema HD 720p · cinema-hd6275481
Cinema Full HD 1080p · cinema-fullhd1568135203

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.

Example — text-to-video (cURL)

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"
  }'

Example — image-to-video (cURL)

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"
  }'

Example — Node.js

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();

Example — Python

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'])

Response (202 Accepted)

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"
}

Get a video

GET/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.

Example

curl https://peekaai.com/api/v1/videos/42 \
  -H "Authorization: Bearer pk_live_YOUR_KEY"

Response

{
  "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"
}

Polling helper — JavaScript

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));
  }
}

Reference assets (persistent)

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.

POST/api/v1/assetsmultipart/form-data · field name: file · jpg/png/webp/gif/heic, ≤20 MB · 0 credits

Asset moderation can take 5–10 seconds. 422 means the content was rejected — check reason_code.

Upload an asset

curl -X POST https://peekaai.com/api/v1/assets \
  -H "Authorization: Bearer pk_live_YOUR_KEY" \
  -F "file=@/path/to/photo.jpg"

Response

{
  "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"
}

Use the asset across multiple videos

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.

Webhook callbacks

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.

  • HTTPS only. We reject HTTP and private/loopback addresses.
  • Body signed with HMAC-SHA256 in X-Peeka-Signature. Secret is shown once when you create the API key — save it.
  • Return any 2xx status to acknowledge. Non-2xx triggers retries: 30s, 2m, 10m, 30m, 2h (max 5 attempts). 4xx is permanent failure.
  • Each delivery has a unique X-Peeka-Delivery header — use it for idempotency.
  • The 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.

Request with callback

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"
  }'

Webhook payload (what we POST to you)

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"
}

Verify signature — Node.js (Express)

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
});

Verify signature — Python (Flask)

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 ack

Lost your secret? Open Account → API tab and click "Rotate webhook secret" on the key.

Run a failed clip again

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.

POST/api/v1/videos/{id}/retryno body · API price · max 1 retry per original task

Eligible error_code values: upstream_busy, upstream_unavailable, upstream_timeout, upstream_error. Returns 400 not_retryable for everything else.

Example

curl -X POST https://peekaai.com/api/v1/videos/42/retry \
  -H "Authorization: Bearer pk_live_YOUR_KEY"

Response (202)

{
  "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).

Batch submission

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.

POST/api/v1/videos/batchmax 5 items · validation upfront · 207 on success

Body: { 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.

Example — 3-scene storyboard sharing one reference asset

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" }
    ]
  }'

Response (207 Multi-Status)

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"
}

Idempotency

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: true

Errors

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"
}
HTTPcodeMeaning
400missing_inputNeither prompt nor image_urls / asset_ids provided
400invalid_model / invalid_ratio / invalid_duration / invalid_resolution / resolution_model_mismatchUnknown or inconsistent parameter; the body lists the allowed values
400invalid_image_url / invalid_callback_url / invalid_asset_id / too_many_imagesURL failed the safety check, asset not yours / not moderated, or more than 5 images
400invalid_json / batch_too_large / invalid_idempotency_keyMalformed request
401invalid_auth_header / invalid_api_keyMissing, revoked, or invalid API key
402insufficient_creditsBalance too low (required / balance included); user_action links to top-up
404not_foundVideo / asset ID does not exist or belongs to another account
409idempotency_in_progressThe first request with this Idempotency-Key is still running; retry after retry_after
409media_not_readySigned link: clip finished but still being saved; retry in a few seconds
409already_renderingCancel too late: the clip is already being made; it will finish and is charged
409already_retried / retry_in_progressThis clip was already run again, or a retry is being started
410link_expiredSigned link older than 7 days; fetch the clip again for a fresh one
422consent_requiredimage_urls sent without subject_consent; nothing was charged
422photo_rejected / moderation_rejectedThe photo failed Peeka moderation (reason_code included); nothing was charged
422image_fetch_failedWe could not download an image_url; upload it with POST /api/v1/assets instead
422idempotency_key_reusedSame Idempotency-Key with a different body; use a new key
429rate_limit_exceeded60 requests/min per key; wait retry_after seconds (Retry-After header)
503moderation_pending / upstream_busy / no_session_available / generation_pausedTemporary; nothing was charged. Retry after retry_after seconds
500task_create_failed / internal_errorOur side; credits refunded. Retry with the same Idempotency-Key and quote trace_id to support

Async failure codes (GET /api/v1/videos/{id} → error_code)

When a task ends in status: "error", the response includes error_code for programmatic handling. Same codes appear in webhook payloads.

error_codeMeaning + retry guidance
image_fetch_failedOur 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_rejectedPrompt or image rejected by content moderation. Retrying won't help — adjust the input.
upstream_busyRate limit / queue overload on the generation backend. Retry with exponential backoff.
upstream_unavailable5xx from the generation backend. Retry after 1–2 minutes.
upstream_timeoutGeneration exceeded our polling timeout (10 min). Credits already refunded — retry.
upstream_auth_failedOur backend credentials broke — please email support, do not retry.
upstream_bad_request / upstream_errorOther upstream rejection — see error field for details.

Assistants (MCP)

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 Code

claude mcp add --transport http peeka https://peekaai.com/mcp

Paste this into your coding agent

Use 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.

Need help?

Questions, higher rate limits or bug reports: email us.

contact@peekaai.com