# Peeka API

Peeka turns one photo and one line into a short cinematic clip with sound, and makes still images (cards, posters, product photos, photo edits and AI characters; see [Images](#images)). This is the plain-text reference for the REST API and for connecting AI assistants. Machine-readable version: https://peekaai.com/openapi.json

- Base URL: `https://peekaai.com/api/v1`
- Auth: `Authorization: Bearer pk_live_…` (create keys in Account → API at https://peekaai.com/account?tab=api)
- Format: JSON in, JSON out. Errors are `application/problem+json` (RFC 9457).
- Typical generation time: 1–2 minutes per clip.

Using Claude, ChatGPT or another assistant? You don't need an API key: connect the remote MCP server at `https://peekaai.com/mcp` (see "Assistants (MCP)" below and https://peekaai.com/connect).

## Quick start

```bash
curl -X POST https://peekaai.com/api/v1/videos \
  -H "Authorization: Bearer pk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7d1c2c9e-0c9a-4a53-9f2b-3f6f1f1b8a11" \
  -d '{"prompt": "A slow drone shot rising over Dubai Marina at sunset", "duration": 5, "ratio": "9:16", "model": "standard"}'
```

Response `202`:

```json
{
  "id": 42,
  "status": "pending",
  "credits_used": 9,
  "duration": 5,
  "ratio": "9:16",
  "model": "standard",
  "estimated_time_seconds": 70,
  "poll_url": "https://peekaai.com/api/v1/videos/42"
}
```

Poll `poll_url` every 5–10 seconds until `status` is `done` or `error`:

```json
{
  "id": 42,
  "status": "done",
  "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=…",
  "credits_used": 9,
  "refunded": false,
  "poll_url": "https://peekaai.com/api/v1/videos/42"
}
```

`video_url` and `poster_url` are absolute signed links. Anyone with the link can open them for 7 days; no key or login needed. They stop working when the clip is deleted. Call `GET /videos/{id}` again for a fresh link.

## Keys and limits

- Unlocking API keys needs a balance of at least 240 credits. A key keeps working while the balance is at least 10 credits (below that every call returns `402 insufficient_credits`).
- Up to 5 keys per account. 60 requests per minute per key. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.

## Prices

Credits per second of video (web price): standard 480p **2**, HD 720p **4**, Full HD 1080p **10**; cinema 480p **3**, HD 720p **6**, Full HD 1080p **15**. The API gets **10% off**: cost = credits per second × seconds × 0.9, rounded up. Lengths: Standard 4–15 s, Cinema up to 30 s (any whole second from 4).

| Tier | `model` | 5 s | 10 s | 15 s | 30 s |
|---|---|---|---|---|---|
| Standard 480p | `standard` | 9 | 18 | 27 | – |
| Standard HD 720p | `standard-hd` | 18 | 36 | 54 | – |
| Standard Full HD 1080p | `standard-fullhd` | 45 | 90 | 135 | – |
| Cinema 480p | `cinema` | 14 | 27 | 41 | 81 |
| Cinema HD 720p | `cinema-hd` | 27 | 54 | 81 | 162 |
| Cinema Full HD 1080p | `cinema-fullhd` | 68 | 135 | 203 | 405 |

Cinema is our newest model: more natural motion and light, clips up to 30 seconds, and characters can speak (including Arabic, with lip sync; put the exact line in quotes, for example `A man at the souk says «أهلًا وسهلًا»`; quoted lines are kept word for word). Standard models accept 4–15 seconds, cinema models 4–30. Model IDs from earlier API versions are still accepted; responses always use the ids above. Current prices without a key: `GET https://peekaai.com/api/v1/pricing`. Failed generations are refunded automatically. Credits are bought on https://peekaai.com/recharge (packs from 19 AED = 40 credits; credits never expire).

## Retries without double charges: `Idempotency-Key`

Send `Idempotency-Key: <any unique string up to 255 characters>` on `POST /videos` and `POST /videos/batch`.

- Same key + same body within 24 hours → the original response, with header `Idempotent-Replayed: true`. Charged once.
- Same key while the first request is still running → `409 idempotency_in_progress` (retry after `retry_after` seconds).
- Same key + different body → `422 idempotency_key_reused`.
- If the first request failed before a task existed (validation, moderation, 402, busy), the key is released and you can retry with it.

## Photos of people (consent)

Reference photos are checked by Peeka moderation before anything is generated.

- `image_urls` (public http/https URLs): Peeka downloads each image itself, runs the same moderation as the web upload and generates from the moderated copy. **You must send `subject_consent`**:
  - `"self"` — the person in the photo is you, or the end user you act for (you are responsible for that under the terms);
  - `"permission"` — the person agreed to this use;
  - `"no_real_person"` — objects, pets, artwork.
  Without it the request fails with `422 consent_required` and nothing is charged. A photo that fails moderation returns `422 photo_rejected` (with `reason_code`); if the check takes longer than about 40 seconds you get `503 moderation_pending` with the `asset_ids` to use later.
- `asset_ids` (from `POST /assets`): already moderated. Send `subject_consent` with them too; requests without it get a `subject_consent_missing` warning and it may become required.
- `asset://` URIs are only accepted if they belong to your own moderated assets.
- Only use photos of people who agreed. Each consent is recorded with the request.

## Endpoints

| Method | Path | What it does |
|---|---|---|
| POST | `/videos` | Create a clip (202) |
| POST | `/videos/batch` | Create up to 5 clips; all-or-nothing validation and charge (207) |
| GET | `/videos/{id}` | Status and signed links |
| GET | `/videos` | List (`limit` ≤ 100, `offset`, `status`) |
| DELETE | `/videos/{id}` | Cancel (refund) while still queued; `409 already_rendering` once it is being made (it finishes and is charged). Finished, failed or cancelled clips are deleted |
| POST | `/videos/{id}/retry` | Run a failed clip again once after a platform failure; charged like a new request (the failed one was refunded) |
| POST | `/assets` | Upload a reference photo (multipart `file`, optional `subject_consent`) |
| GET | `/assets` | List moderated photos |
| GET | `/assets/{id}` | Photo status (`pending`, `trusted`, `rejected`) |
| DELETE | `/assets/{id}` | Delete a photo |
| POST | `/images` | Make images (202): description, template, photo edit or AI character |
| GET | `/images/{id}` | Image job status and links (`?wait=` up to 30 s) |
| GET | `/images` | List image jobs (`limit` ≤ 50, `before`) |
| DELETE | `/images/{id}` | Delete the images of a job |
| GET | `/account` | Balance and usage |
| GET | `/pricing` | Prices (no key) |
| GET | `/media/{id}.mp4` / `/media/{id}.jpg` | Signed links from `video_url` / `poster_url` (no key) |

### POST /videos — body

| Field | Type | Notes |
|---|---|---|
| `prompt` | string | What happens in the clip. English works best. Required unless images are given. |
| `duration` | integer | Seconds, default 5: 4–15 for standard models, 4–30 for cinema models |
| `ratio` (`aspect_ratio`) | string | `16:9` (default), `9:16`, `1:1`, `4:3`, `3:4`, `21:9` |
| `model` | string | `standard` (480p, default), `standard-hd` (720p), `standard-fullhd` (1080p), `cinema`, `cinema-hd`, `cinema-fullhd` |
| `resolution` | string | Optional `480p` / `720p` / `1080p`; must match the model |
| `asset_ids` | integer[] | Up to 5, from `POST /assets`. Takes priority over `image_urls`. |
| `image_urls` | string[] | Up to 5 public image URLs. Needs `subject_consent`. |
| `subject_consent` | string | `self`, `permission` or `no_real_person` |
| `callback_url` | string | HTTPS URL for the signed webhook |

### POST /videos/batch

```json
{
  "defaults": { "model": "standard", "duration": 5, "ratio": "9:16", "asset_ids": [1234], "subject_consent": "self" },
  "items": [
    { "prompt": "Scene 1: she walks into the cafe" },
    { "prompt": "Scene 2: she orders coffee, smiling" }
  ]
}
```

Every item is validated (and its photos moderated) before anything is charged. If one item is invalid the whole batch is rejected with `item_index`. Returns `207` with one entry per item.

### POST /assets

```bash
curl -X POST https://peekaai.com/api/v1/assets \
  -H "Authorization: Bearer pk_live_YOUR_KEY" \
  -F "file=@me.jpg" -F "subject_consent=self"
```

jpg, png, webp, gif or heic up to 20 MB. If `status` is `pending`, poll `GET /assets/{id}` until `trusted` (usually 5–60 seconds), then use the id in `asset_ids`. Uploading the same file again returns the same asset.

## Images

Make still images from a description, from a ready-made template (cards, headshots, product photos, posters), by editing up to 3 photos, or as an **AI character**: 4 photos of one made-up adult that can star in clips.

```bash
curl -X POST https://peekaai.com/api/v1/images \
  -H "Authorization: Bearer pk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5b0e6c1e-img-0001" \
  -d '{"description": "A cup of Arabic coffee and dates on a wooden table, soft morning light", "aspect_ratio": "4:5", "count": 4}'
```

Response `202`:

```json
{
  "id": 118,
  "object": "image_job",
  "status": "generating",
  "type": "image",
  "quality": "standard",
  "aspect_ratio": "4:5",
  "count": 4,
  "credits_reserved": 7,
  "credits_charged": null,
  "images": [],
  "estimated_time_seconds": 45,
  "poll_url": "https://peekaai.com/api/v1/images/118"
}
```

Call `GET /images/{id}?wait=30` (it answers as soon as the job finishes, or after 30 s) until `status` is `done`, `partial` or `failed`:

```json
{
  "id": 118,
  "status": "done",
  "credits_reserved": 7,
  "credits_charged": 7,
  "credits_refunded": 0,
  "images": [
    { "id": 402, "url": "https://peekaai.com/uploads/…/img/….jpg", "thumbnail_url": "https://peekaai.com/uploads/…/img/….thumb.webp", "width": 1296, "height": 1620, "format": "jpeg", "animate": "maybe", "trusted_until": null }
  ]
}
```

Most images take 30–60 seconds; an AI character takes about 2 minutes (the 4 photos arrive one by one).

### Prices

| What | Body | 1 image | 4 variations |
|---|---|---|---|
| Standard (about 2.4 megapixels) | `"quality": "standard"` (default) | 2 | 7 |
| HD (about 4.2 megapixels) | `"quality": "hd"` | 4 | 14 |
| AI character: 4 photos of the same made-up adult | `"type": "character"` | 5 | — |

Credits are reserved when the job starts. If some images can't be made (for example a result is blocked by content rules), the unused part is refunded when the job ends and `credits_charged` shows the final amount. A job with no images costs nothing. Deleting images does not refund credits.

### POST /images — body

| Field | Type | Notes |
|---|---|---|
| `description` (`prompt`) | string | What to make, up to 1000 characters. For edits: what to change. English, Arabic or Chinese. Required unless `template` is set. |
| `type` | string | `image` (default) or `character` |
| `template` | string | `eid`, `diwali`, `national_day`, `festive`, `celebration`, `headshot`, `product`, `poster` (see below) |
| `fields` | object | Template fields |
| `text` | string | Up to 60 characters drawn into the image (English, Arabic, Chinese and most European and East Asian languages). The letters are drawn by AI, so spelling isn't guaranteed: avoid long names and dates. |
| `aspect_ratio` | string | `1:1` (default), `9:16`, `4:5`, `16:9`, `3:4` |
| `quality` | string | `standard` (default) or `hd` |
| `count` | integer | `1` (default) or `4` variations |
| `character` | object | For `type: "character"`: `{"age": "18-25" \| "26-35" \| "36-50" \| "51+", "style": "photo" \| "cinematic" \| "animated" \| "illustrated"}`. Describe the look and outfit in `description`. No photos. |
| `asset_ids` | integer[] | Up to 3 photos from `POST /assets` to edit or combine |
| `image_urls` | string[] | Up to 3 public image URLs; needs `subject_consent` (downloaded and moderated like `/videos`) |
| `subject_consent` | string | Who is in the photos: `self`, `permission` or `no_real_person` |
| `max_credits` | integer | Optional. Refuse (`422 cost_exceeds_max`) instead of charging more |

Send `Idempotency-Key` on `POST /images` exactly as on `POST /videos`.

### Templates

| `template` | `fields` | Photos |
|---|---|---|
| `eid` | `style`: `emerald_gold`, `pastel`, `midnight`, `minimal`; `greeting` (text) | — |
| `diwali` | `style`: `warm_gold`, `rangoli`, `maroon`; `greeting` | — |
| `national_day` | `style`: `ribbons`, `skyline`, `desert`; `greeting` | — |
| `festive` | `occasion`: `christmas`, `new_year`, `lunar_new_year`; `look`: `classic`, `modern`, `illustrated`; `greeting` | — |
| `celebration` | `occasion`: `birthday`, `wedding`, `new_baby`, `graduation`, `congrats`; `look`: `elegant`, `playful`, `floral`; `greeting` | — |
| `headshot` | `outfit`: `blazer`, `smart_casual`, `modest`, `keep`; `background`: `studio`, `office`, `outdoor` | 1 photo of the person (`subject_consent` `self` or `permission`) |
| `product` | `scene`: `marble`, `desert`, `studio`, `botanical`, `dark` | 1–3 photos of the product |
| `poster` | `headline` (required), `subject` (text), `look`: `bold`, `luxury`, `playful`, `minimal` | — |

Cards never show flags, official emblems, places of worship or religious text; leave `greeting` empty for a card with clean space for your own text.

### From image to clip: `animate`

Each image says whether it can become a clip:

- `trusted` — an AI character photo. It works as a clip photo until `trusted_until` (30 days): pass its `url` in `image_urls` of `POST /videos` with `"subject_consent": "no_real_person"`, or its id in the MCP tool `create_clip` (`image_ids`).
- `ok` — it can become a clip: no people in it, or made from your own photos (the people in them must have agreed). Use it as a reference photo for a clip.
- `maybe` — it may show people; a clip made from it may be refused.
- `no` — edited from a photo of a real person at a time when such images can't become clips. It stays an image.

### Image links and rules

- `url` is a permanent, unguessable link to the original file (PNG when the image has text, otherwise JPEG), with the file's original metadata kept. It stops working when the images are deleted. `thumbnail_url` is a small WebP preview.
- Images are AI-generated. Label them as AI-generated when you publish them.
- Not allowed: documents, IDs, receipts, invoices or money (`422 document_blocked`); religious phrases and sacred names drawn by AI (`422 religious_text`); AI characters under 18 (`422 minor_blocked`). Text in South Asian scripts isn't supported yet (`422 text_script_unsupported`).
- Failed jobs have `status: "failed"` and an `error_code`: `prompt_blocked`, `upload_blocked`, `result_blocked`, `copyright_blocked`, `real_person_blocked`, `document_blocked` (change the input), `busy`, `timeout`, `failed` (try again). Nothing is charged for them.
- At most 3 image jobs run at a time per account, and 30 jobs per 10 minutes.

## Webhooks

Pass `callback_url` and we POST when the clip is done or failed.

- Headers: `X-Peeka-Event` (`video.completed` or `video.failed`), `X-Peeka-Signature` (`sha256=` + hex HMAC-SHA256 of the raw body, keyed with the webhook secret shown once when you created the key), `X-Peeka-Delivery` (unique per delivery; use it to deduplicate).
- Reply with any 2xx. Other responses are retried after 30 s, 2 min, 10 min, 30 min and 2 h; 4xx is treated as permanent.
- Payload fields: `event`, `id`, `status`, `prompt`, `video_url`, `error`, `error_code`, `credits_used`, `duration`, `model`, `created_at`, `completed_at`, `delivered_at`.
- The `video_url` in the webhook opens for a signed-in browser. For a link that works anywhere, call `GET /videos/{id}` and use its signed `video_url`.

## Errors

Every error is `application/problem+json`:

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

- `code` is stable; `error` is the old human message (kept for older clients).
- `retryable: true` means the same request can succeed later; wait `retry_after` seconds (also sent as `Retry-After`).
- `user_action` is set when a person has to do something: `top_up`, `confirm_consent`, `raise_budget`, `upload_photo`, `contact_support`.
- 5xx responses never include internal details; quote `trace_id` to support.

| HTTP | code | Meaning | Retry? |
|---|---|---|---|
| 400 | `missing_input` | No prompt and no images | Fix input |
| 400 | `invalid_duration`, `invalid_ratio`, `invalid_model`, `invalid_resolution`, `resolution_model_mismatch` | Bad parameter | Fix input |
| 400 | `too_many_images`, `invalid_image_url`, `invalid_asset_id`, `invalid_callback_url` | Bad images or URLs | Fix input |
| 400 | `batch_too_large`, `invalid_json`, `invalid_idempotency_key` | Bad request | Fix input |
| 401 | `invalid_auth_header`, `invalid_api_key` | Missing or wrong key | No |
| 402 | `insufficient_credits` | Balance too low (`required`, `balance`) | After top-up |
| 404 | `not_found` | Unknown or not yours | No |
| 409 | `idempotency_in_progress` | Same key still running | Yes |
| 409 | `already_rendering` | Cancel too late: the clip is already being made and will finish | No |
| 409 | `already_retried`, `retry_in_progress`, `media_not_ready` | Retry used or starting / file still saving | `media_not_ready`: yes |
| 410 | `link_expired` | Signed link expired | Get a fresh link |
| 413 | `too_large`, `body_too_large` | File or body too big | Fix input |
| 422 | `consent_required` | `image_urls` without `subject_consent` | Fix input |
| 422 | `photo_rejected`, `moderation_rejected` | Photo failed moderation | Use another photo |
| 422 | `image_fetch_failed` | We couldn't download `image_urls` | Upload via `/assets` |
| 422 | `idempotency_key_reused` | Same key, different body | Use a new key |
| 422 | `prompt_blocked`, `document_blocked`, `religious_text`, `minor_blocked`, `text_script_unsupported`, `cost_exceeds_max` | Image request not allowed | Fix input |
| 400 | `preset_not_found`, `field_required`, `photos_required`, `character_no_photos` | Bad image template input | Fix input |
| 429 | `concurrency_limit`, `rate_limited` | Too many image jobs at once | Yes |
| 429 | `rate_limit_exceeded` | 60 requests/minute per key | Yes |
| 451 | `region_unavailable` | Creating isn't available where this account was last used on peekaai.com | After opening peekaai.com from a supported country (VPN off) |
| 503 | `upstream_busy`, `no_session_available`, `moderation_pending`, `generation_paused` | Temporary; nothing charged | Yes |
| 500 | `task_create_failed`, `internal_error` | Our side; credits refunded | Yes (same Idempotency-Key) |

A task that fails after it started has `status: "error"`, an `error_code` and a plain `error` sentence (for people; build on the code, the text can change): `content_rejected`, `copyright_rejected`, `output_rejected` or `real_person_rejected` (change the input), `task_config_error` (change the settings), `image_fetch_failed` (use `asset_ids`), `upstream_busy`, `upstream_unavailable`, `upstream_timeout`, `upstream_error` (can be run again once with `POST /videos/{id}/retry`, charged like a new request), `upstream_auth_failed` (contact us). Credits for failed tasks are refunded automatically. Reference video and audio inputs are not offered yet (the web app answers `400 reference_media_unavailable`); use photos.

## Assistants (MCP)

Peeka runs a remote MCP server for AI assistants at **`https://peekaai.com/mcp`** (Streamable HTTP, stateless; MCP 2026-07-28 with 2025 clients supported).

It is live. `GET https://peekaai.com/api/agent/status` shows whether it is currently enabled.

- Sign-in is OAuth 2.1 with PKCE against the user's own Peeka account. Discovery: `https://peekaai.com/.well-known/oauth-protected-resource/mcp` and `https://peekaai.com/.well-known/oauth-authorization-server`. Dynamic client registration and Client ID Metadata Documents are supported.
- On the consent screen the user sets a daily credit limit for that assistant (default 30 credits per 24 hours; 10 until the account has topped up once). They can change it or disconnect at https://peekaai.com/account/assistants.
- Assistants pay the normal app price (no API discount): 2 credits per second standard, 4 per second HD.
- Every spending tool needs `max_credits`; the server refuses instead of charging more. At most 3 clips from one assistant run at a time. Retries with the same arguments within 2 minutes (or the same `idempotency_key`) never charge twice.
- Assistants cannot send photos. `start_photo_upload` returns a link where the person uploads the photo on Peeka and confirms who is in it; only moderated, consented photos can be used.
- Developers can also call `/mcp` with `Authorization: Bearer pk_live_…` (same per-key daily limit, shown in Connected assistants).

Tools: `list_effects`, `get_account`, `start_photo_upload`, `get_photo`, `list_photos`, `create_clip_from_effect`, `create_clip` (photos from `start_photo_upload` and/or `image_ids` from `create_image`), `get_clip` (waits up to 45 s), `list_clips`, `cancel_clip`, `create_image` (images and AI characters; same `max_credits` and daily-limit rules; waits up to 45 s), `get_image`, `get_topup_link`.

Add it in Claude Code: `claude mcp add --transport http peeka https://peekaai.com/mcp`

## Paste this into your coding agent

```text
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.
For still images use POST /images (same Idempotency-Key rules) and GET /images/{id}?wait=30.
Errors are RFC 9457 problem+json: respect retryable and retry_after, and show user_action.url to the user.
```

## Support

contact@peekaai.com — include the `trace_id` from the error.
