# PlayAI API Reference

Base URL: `https://playablelab.com`  
Support: [support@playablelab.com](mailto:support@playablelab.com)

## Models

Send exactly one `model` per request.

| `model` | Output |
|---|---|
| `PlayAIV` | Video (MP4) |
| `PlayAIP` | Interactive ad (HTML preview + optional network export package) |
| `PlayAII` | Still image / banner |

## Authentication

Create an API key in the dashboard: profile menu → **Account → API keys** → **Create key**. The full key is shown once. Up to **5** active keys per account.

```
Authorization: Bearer plk_live_YOUR_KEY
```

Use the Bearer header on every request. Do not expose keys in client apps.

The account must have **at least 1 credit** before a job is accepted.

## Endpoints

| Action | Method | Path |
|---|---|---|
| Start a job | `POST` | `/api/public/v1/run` |
| Job status | `GET` | `/api/public/v1/runs/{id}` |
| Recent jobs | `GET` | `/api/public/v1/runs` |
| Download main file | `GET` | `{fileUrl}` from succeeded status |
| Download companion | `GET` | `{companionUrl}` when `hasCompanion` is true |

## Job lifecycle

1. `POST /run` with one `model` and that model’s required fields.
2. Response includes `id` and `status: "queued"`. `creditsCharged` is **0**.
3. Poll `GET /runs/{id}` until `status` is `succeeded` or `failed`.
4. On `succeeded`, download via `fileUrl` (and `companionUrl` if present) using the same Bearer key.
5. After `succeeded` or `failed`, you may start another `POST /run`. Download is not required first.

**Concurrency:** One unfinished job per account (`queued` or `processing`). A second `POST /run` returns `JOB_IN_PROGRESS` (409) with the existing job `id`.

**Billing:** Credits are charged only when `ok` is `true` and `status` is `"succeeded"`. All rejects and failures return `creditsCharged: 0`.

If `POST /run` times out before you receive an `id`, call `GET /runs` and match the newest run by `createdAt` before retrying.

## Retention and download

- Up to **3** completed results remain available for download per account. Older results are removed when a newer one completes.
- Download does not remove a result while it is still available.
- Store files on your side if you need them longer.
- Relative `fileUrl` values are resolved against `https://playablelab.com`.

## Global limits

| Field / rule | Limit |
|---|---|
| `prompt` | **15–4000** characters after trim (any language) |
| `images` | **0–10** optional reference stills on the same request |
| Each image (decoded) | **≤ 2 MB** |
| All images together (decoded) | **≤ 20 MB** |
| Whole `POST /run` body | **≤ 16 MB** |
| Image pixels | Shortest side **≥ 512 px**, longest side **≤ 4096 px** |
| Image types | PNG, JPEG, WebP |
| Video `duration` | Integer **5–60** seconds in the request; maximum allowed value depends on your plan (see PlayAIV) |
| `cta` (PlayAIP) | `http://` or `https://`, max **2048** characters |
| `GET /runs` | Up to **20** recent jobs, newest first |

Send one value per field. Unknown keys are rejected.

## PlayAIV — video

```http
POST /api/public/v1/run
Authorization: Bearer plk_live_YOUR_KEY
Content-Type: application/json
```

```json
{
  "model": "PlayAIV",
  "prompt": "A golden retriever running through a field of sunflowers on a sunny day",
  "aspect_ratio": "9:16",
  "duration": 15,
  "generate_audio": true,
  "quality": "720p"
}
```

| Field | Required | Values |
|---|---|---|
| `model` | yes | `PlayAIV` |
| `prompt` | yes | 15–4000 characters |
| `aspect_ratio` | yes | `9:16` · `16:9` · `1:1` · `4:5` |
| `duration` | yes | Integer seconds; delivered MP4 length. Plan maximum: Free **5** · Starter **15** · Pro **60** |
| `generate_audio` | yes | `true` · `false` |
| `quality` | yes | `720p` · `1080p` (`1080p` requires Pro) |
| `images` | no | 0–10 reference stills (see Global limits) |

Do not send `network` or `cta`.

Accepted response:

```json
{
  "ok": true,
  "status": "queued",
  "id": "run_01HZX...",
  "model": "PlayAIV",
  "creditsCharged": 0,
  "creditBalance": 43.32
}
```

## PlayAIP — interactive ad

```json
{
  "model": "PlayAIP",
  "prompt": "A short fashion interactive ad: tap to reveal the jacket, then an end card",
  "aspect_ratio": "9:16",
  "generate_audio": true,
  "network": "applovin",
  "cta": "https://example.com/product"
}
```

| Field | Required | Values |
|---|---|---|
| `model` | yes | `PlayAIP` |
| `prompt` | yes | 15–4000 characters |
| `aspect_ratio` | yes | `9:16` · `16:9` · `auto` |
| `generate_audio` | yes | `true` · `false` |
| `network` | yes | `applovin` · `unityads` · `mintegral` · `google` · `meta` · `moloco` · `tiktok` · `vungle` · `web` |
| `cta` | yes | Click-out URL (`http://` or `https://`) |
| `images` | no | 0–10 reference stills |

Do not send `duration` or `quality`.

- `network: "web"` is available on all plans.
- Other networks require Starter or Pro.
- `auto` resolves to `9:16` or `16:9` in the response.

When a network export is produced, the succeeded response includes `hasCompanion: true` and `companionUrl`. Download both `fileUrl` (preview) and `companionUrl` (package).

## PlayAII — image

```json
{
  "model": "PlayAII",
  "prompt": "Clean product hero on a stone surface, soft daylight, room for a logo",
  "aspect_ratio": "1:1",
  "quality": "720p"
}
```

| Field | Required | Values |
|---|---|---|
| `model` | yes | `PlayAII` |
| `prompt` | yes | 15–4000 characters |
| `aspect_ratio` | yes | `9:16` · `16:9` · `1:1` · `4:5` |
| `quality` | yes | `720p` · `1080p` (`1080p` requires Pro) |
| `images` | no | 0–10 reference stills |

Do not send `duration`, `generate_audio`, `network`, or `cta`.

## Optional reference images

Add `images` on the same `POST /run` body:

```json
"images": [
  { "mime": "image/jpeg", "base64": "<standard-base64>" }
]
```

JSON body only — no multipart upload.

## Status responses

### In progress

```json
{
  "ok": true,
  "status": "queued",
  "id": "run_01HZX...",
  "model": "PlayAIV",
  "creditsCharged": 0,
  "creditBalance": 43.32
}
```

`status` may be `queued` or `processing`. Poll until terminal.

### Succeeded

```json
{
  "ok": true,
  "status": "succeeded",
  "id": "run_01HZX...",
  "model": "PlayAIV",
  "kind": "video",
  "title": "...",
  "summary": "...",
  "aspectRatio": "9:16",
  "quality": "720p",
  "durationSec": 15,
  "fileUrl": "/api/public/v1/results/res_...",
  "fileName": "video.mp4",
  "mimeType": "video/mp4",
  "hasCompanion": false,
  "creditsCharged": 0.42,
  "creditBalance": 42.9
}
```

Download with the same Bearer key.

### Failed

```json
{
  "ok": false,
  "status": "failed",
  "id": "run_01HZX...",
  "error": "GENERATION_FAILED",
  "creditsCharged": 0,
  "creditBalance": 43.32
}
```

### Recent jobs — `GET /api/public/v1/runs`

Metadata only (no file bytes). Includes `queued`, `processing`, `succeeded`, and `failed`.

```json
{
  "ok": true,
  "runs": [
    {
      "id": "run_01HZX...",
      "model": "PlayAIV",
      "kind": "video",
      "status": "queued",
      "createdAt": "2026-09-02T15:04:00.000Z",
      "creditsCharged": 0
    }
  ]
}
```

## Errors

Reject shape (valid key):

```json
{
  "ok": false,
  "error": "PROMPT_TOO_SHORT",
  "creditsCharged": 0,
  "creditBalance": 43.32
}
```

Invalid key (no balance field):

```json
{
  "ok": false,
  "error": "INVALID_API_KEY",
  "creditsCharged": 0
}
```

| HTTP | `error` | Meaning |
|---|---|---|
| 401 | `INVALID_API_KEY` | Missing or invalid key |
| 403 | `ACCOUNT_BLOCKED` | Account not active |
| 402 | `INSUFFICIENT_CREDITS` | Balance below 1 (`creditsRequired: 1` may be included) |
| 400 | `INVALID_INPUT` | Invalid or unknown fields |
| 400 | `PROMPT_TOO_SHORT` | Prompt under 15 characters |
| 400 | `PROMPT_TOO_LONG` | Prompt over 4000 characters |
| 400 | `REF_TOO_SMALL` | Image shortest side under 512 px |
| 400 | `REF_TOO_LARGE` | Image file or dimensions over limit |
| 400 | `DURATION_NOT_SUPPORTED` | Duration outside allowed range for your plan |
| 400 | `REQUEST_TOO_LARGE` | Request body over 16 MB |
| 403 | `QUALITY_NOT_ALLOWED` | `1080p` not available on your plan |
| 403 | `NETWORK_NOT_ALLOWED` | Network requires Starter or Pro |
| 404 | `NOT_FOUND` | Unknown or unavailable result |
| 409 | `JOB_IN_PROGRESS` | Unfinished job exists; response includes existing job `id` |

## Node.js example

JSON endpoints and file downloads use the same Bearer key but different helpers — API routes return JSON; `fileUrl` returns the raw file bytes (MP4, image, or HTML package).

```js
const BASE = 'https://playablelab.com';
const KEY = process.env.PLAYAI_API_KEY;

async function api(path, { method = 'GET', body } = {}) {
  const res = await fetch(`${BASE}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${KEY}`,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  return res.json();
}

async function download(fileUrl) {
  const url = fileUrl.startsWith('http') ? fileUrl : `${BASE}${fileUrl}`;
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${KEY}` },
  });
  if (!res.ok) throw new Error(`Download failed: HTTP ${res.status}`);
  return res.arrayBuffer();
}

async function waitForRun(id) {
  for (;;) {
    const run = await api(`/api/public/v1/runs/${id}`);
    if (run.status === 'succeeded' || run.status === 'failed') return run;
    await new Promise((r) => setTimeout(r, 4000));
  }
}

const accept = await api('/api/public/v1/run', {
  method: 'POST',
  body: {
    model: 'PlayAIV',
    prompt: 'A golden retriever running through a field of sunflowers on a sunny day',
    aspect_ratio: '9:16',
    duration: 15,
    generate_audio: true,
    quality: '720p',
  },
});

if (!accept.ok) {
  console.error(accept.error);
} else {
  const result = await waitForRun(accept.id);
  if (result.ok && result.status === 'succeeded') {
    const bytes = await download(result.fileUrl);
    // e.g. fs.writeFileSync('out.mp4', Buffer.from(bytes))
  }
}
```
