# Vixdeo API — Quickstart

> Public, versioned surface mounted at `/v1`. All snippets below
> assume you have:
>
> 1. An organization, with at least one channel connected.
> 2. An API key with `write` scope (`vbp_*`).
> 3. The base URL: `https://vixdeo.com/v1` (there is no separate
>    `api.` host and no public staging environment).

This walkthrough takes you from "I have a key" to "I have a finished
video URL" in three calls. If your client is an **agent**, the same
contract is also exposed as a native MCP server — see
[Agents (MCP)](#agents-mcp) at the end.

---

## 1. Sanity-check your key

A read-only call against `/v1/usage` confirms the key is valid and
shows your remaining quota:

### cURL

```bash
curl -sS \
  -H "Authorization: Bearer $VIXDEO_API_KEY" \
  https://vixdeo.com/v1/usage
```

### Python (`requests`)

```python
import os, requests

base = "https://vixdeo.com/v1"
headers = {"Authorization": f"Bearer {os.environ['VIXDEO_API_KEY']}"}

usage = requests.get(f"{base}/usage", headers=headers, timeout=10).json()
print(f"{usage['videos_remaining']} of {usage['videos_limit']} left "
      f"this period (resets {usage['period_end']})")
```

### JavaScript (`fetch`)

```js
const base = "https://vixdeo.com/v1";
const auth = { Authorization: `Bearer ${process.env.VIXDEO_API_KEY}` };

const r = await fetch(`${base}/usage`, { headers: auth });
const usage = await r.json();
console.log(`${usage.videos_remaining}/${usage.videos_limit} left, resets ${usage.period_end}`);
```

A 200 with a `UsageOut` body means you are good to go. A `401
unauthorized` Problem Details response means the key is missing,
malformed, or revoked. A `403 forbidden` means the key is valid but
lacks the scope this endpoint requires.

---

## 2. Submit your first production

Send a Video Blueprint to `/v1/productions`. The smallest accepted VBP
is two scenes with non-empty `narrator_text`.

### cURL

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $VIXDEO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
        "blueprint": {
          "schema_version": "1.0",
          "title": "How rooftop solar pays for itself",
          "scenes": [
            {
              "narrator_text": "Rooftop solar pays for itself in under seven years on average.",
              "visual_mode": "stock"
            },
            {
              "narrator_text": "Federal tax credits cover thirty percent of installation costs.",
              "visual_mode": "stock"
            }
          ]
        },
        "auto_publish": false
      }' \
  https://vixdeo.com/v1/productions
```

A successful response is `202 Accepted` with:

```json
{
  "production_id": "prod_2g1Z9mB7",
  "status": "queued",
  "deduplicated": false
}
```

### Python

```python
import os, uuid, requests

base = "https://vixdeo.com/v1"
headers = {
    "Authorization": f"Bearer {os.environ['VIXDEO_API_KEY']}",
    "Idempotency-Key": str(uuid.uuid4()),
}
body = {
    "blueprint": {
        "schema_version": "1.0",
        "title": "How rooftop solar pays for itself",
        "scenes": [
            {"narrator_text": "Rooftop solar pays for itself in under seven years on average.",
             "visual_mode": "stock"},
            {"narrator_text": "Federal tax credits cover thirty percent of installation costs.",
             "visual_mode": "stock"},
        ],
    },
    "auto_publish": False,
}
r = requests.post(f"{base}/productions", json=body, headers=headers, timeout=30)
r.raise_for_status()
prod = r.json()
print(prod["production_id"])
```

### JavaScript

```js
import { randomUUID } from "node:crypto";

const base = "https://vixdeo.com/v1";
const headers = {
  Authorization: `Bearer ${process.env.VIXDEO_API_KEY}`,
  "Content-Type": "application/json",
  "Idempotency-Key": randomUUID(),
};
const body = {
  blueprint: {
    schema_version: "1.0",
    title: "How rooftop solar pays for itself",
    scenes: [
      { narrator_text: "Rooftop solar pays for itself in under seven years on average.", visual_mode: "stock" },
      { narrator_text: "Federal tax credits cover thirty percent of installation costs.", visual_mode: "stock" },
    ],
  },
  auto_publish: false,
};
const r = await fetch(`${base}/productions`, {
  method: "POST", headers, body: JSON.stringify(body),
});
if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`);
const { production_id } = await r.json();
console.log(production_id);
```

### Idempotency contract

Every `POST` requires the `Idempotency-Key` header. Retries with the
same key and an identical body replay the original response (the
response carries `Idempotent-Replay: true`). The same key with a
*different* body returns `409 idempotency-conflict`. Records persist
for 24 hours.

A `402 payment-required` Problem Details means your org has run out
of monthly quota; `extensions.period_resets_at` tells you when the
next period starts.

---

## 3. Poll until completion

Productions are asynchronous. Poll `/v1/productions/{id}` until
`status` reaches `completed` (or `failed`):

### cURL (loop)

```bash
PROD_ID=prod_2g1Z9mB7
while :; do
  resp=$(curl -sS -H "Authorization: Bearer $VIXDEO_API_KEY" \
              "https://vixdeo.com/v1/productions/$PROD_ID")
  status=$(echo "$resp" | jq -r .status)
  echo "$status"
  if [ "$status" = "completed" ] || [ "$status" = "failed" ]; then
    echo "$resp" | jq .
    break
  fi
  sleep 10
done
```

### Python

```python
import time

while True:
    r = requests.get(f"{base}/productions/{prod['production_id']}",
                     headers=headers, timeout=10)
    r.raise_for_status()
    detail = r.json()
    if detail["status"] in ("completed", "failed", "cancelled"):
        break
    time.sleep(10)

print(detail["status"], detail.get("final_video_url"))
```

### JavaScript

```js
let detail;
do {
  await new Promise((r) => setTimeout(r, 10_000));
  detail = await fetch(`${base}/productions/${production_id}`, { headers: auth }).then((r) => r.json());
} while (!["completed", "failed", "cancelled"].includes(detail.status));
console.log(detail.status, detail.final_video_url);
```

Polling is fine for one-off scripts. For production integrations,
**use webhooks** instead — see the next section.

---

## 4. Subscribe to lifecycle events (recommended)

Polling 16 times per video doesn't scale. Subscribe once and let
Vixdeo push the lifecycle events to you:

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $VIXDEO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
        "name": "Production lifecycle → ops dashboard",
        "url": "https://hooks.example.com/vixdeo",
        "events": ["production.completed", "production.failed", "production.cancelled"]
      }' \
  https://vixdeo.com/v1/webhooks
```

The 201 response carries the plaintext `secret` **once**. Persist it
immediately — only its SHA-256 lives on our side.

### Verifying signatures

Every delivery includes:

- `X-Vixdeo-Signature: t=<unix_seconds>,v1=<hex_hmac>`
- `X-Vixdeo-Event-Type: production.completed`
- `X-Vixdeo-Event-Id: evt_…` (use this to dedup; same event can be
  delivered to multiple subscriptions)

The signed string is `f"{t}.{raw_request_body}"`, signed with
HMAC-SHA256 using the plaintext secret. **Tolerate 5 minutes of
clock skew** when checking `t`.

#### Python

```python
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str, *, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts["t"]); sig = parts["v1"]
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
```

#### JavaScript

```js
import crypto from "node:crypto";

export function verify(rawBody, header, secret, { tolerance = 300 } = {}) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t), sig = parts.v1;
  if (Math.abs(Date.now() / 1000 - t) > tolerance) return false;
  const mac = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(sig));
}
```

Failed deliveries retry on a backoff of 1m / 5m / 30m / 2h / 12h
before being dead-lettered. You can manually re-trigger any past
attempt with `POST /v1/webhooks/{id}/deliveries/{delivery_id}/replay`.

---

## 5. Error handling

Every non-2xx response uses **RFC 7807 Problem Details** with
`Content-Type: application/problem+json`. Match on `type` (an opaque
URI under `https://vixdeo.com/errors/`) — do not parse `detail`.

| `type` slug | Status | When |
|---|---|---|
| `unauthorized` | 401 | Missing/invalid bearer key |
| `forbidden` | 403 | Key valid but scope or channel-lock denied access |
| `not-found` | 404 | Resource does not exist or is not visible to you |
| `payment-required` | 402 | Monthly quota exhausted |
| `unsupported-vbp-version` | 422 | `schema_version` not recognised |
| `unprocessable-entity` | 422 | Body failed schema validation |
| `idempotency-conflict` | 409 | Same `Idempotency-Key`, different body |
| `rate-limit-exceeded` | 429 | See `Retry-After` header |

Example:

```json
{
  "type": "https://vixdeo.com/errors/payment-required",
  "title": "Payment Required",
  "status": 402,
  "detail": "Plan studio has reached its monthly limit (50/50).",
  "instance": "/v1/productions",
  "extensions": {
    "plan": "studio",
    "videos_used": 50,
    "videos_limit": 50,
    "period_resets_at": "2026-05-01T00:00:00+00:00"
  }
}
```

---

## 6. Rate limits

Limits are per API key and depend on your tier:

| Tier | Productions/min | Productions/day | Other reads/min |
|---|---|---|---|
| Studio | 5 | 50 | 60 |
| Agency | 15 | 250 | 200 |
| Scale | 50 | 1000 | 600 |

Every response carries a Stripe-style header set:

```
X-RateLimit-Limit:     5
X-RateLimit-Remaining: 4
X-RateLimit-Reset:     1714186870
X-RateLimit-Bucket:    productions_per_minute
```

…plus per-bucket triplets like
`X-RateLimit-Limit-productions_per_minute`. A 429 also carries
`Retry-After: <seconds>`. Treat the unsuffixed triplet as the
most-constrained bucket — that's the one you'll trip first.

---

## 7. Agents (MCP)

The same contract a human drives through the chat is exposed to
agents as a **native MCP server** — JSON-RPC 2.0 over Streamable
HTTP, protocol revision `2025-06-18`:

```
POST https://vixdeo.com/v1/mcp
Authorization: Bearer <credential>
```

### Two ways in, one principal

1. **OAuth 2.1** — for hosts that do MCP authorization discovery
   (claude.ai custom connectors, Cursor, ChatGPT, SDKs). Add
   `https://vixdeo.com/v1/mcp` as a remote MCP server; the host
   registers itself (RFC 7591), sends the user to Vixdeo's consent
   screen and receives an access token (PKCE S256, refresh with
   rotation). Discovery documents:
   - `https://vixdeo.com/.well-known/oauth-protected-resource/v1/mcp` (RFC 9728)
   - `https://vixdeo.com/.well-known/oauth-authorization-server` (RFC 8414)
2. **API key** — for programmatic agents. Create it in the app under
   **Settings → Agent access** (name, scopes, optional expiry), then:

```bash
claude mcp add --transport http vixdeo https://vixdeo.com/v1/mcp \
  --header "Authorization: Bearer $VIXDEO_API_KEY"
```

Both resolve to the same principal: your organization, your scopes,
your attribution. Disconnecting an app or revoking a key takes effect
on the next request. A `401` always carries `WWW-Authenticate` with
the `resource_metadata` URL, as the MCP spec requires.

### Tools — the agent closes the loop

| Tool | What it does |
|---|---|
| `list_referents` | The formats/styles a piece can be produced in, plus duration profiles, output formats and budget tiers ($0) |
| `generate_script` | Idea → new project. With `production.auto_produce` (default `true`) the production line runs to the **final video** by itself. Bring your own `source` (closed script narrated verbatim, or an article to adapt) and name the `cast` by `@handle` |
| `list_cast` | The voices and presenters your organization can name, with `@handle` ($0) |
| `get_project_status` | Lifecycle status, runs, cost and `finalVideoUrl` — a signed link that opens and can be shared outside the app for 7 days; `next` tells the agent what to do ($0) |
| `produce` | Start/resume the line of an existing project (same sequence as the human's button; never publishes) |
| `list_projects` | Projects visible to your org |
| `get_project_vbp` | Read a project's Video Blueprint |
| `list_ops` | The edit-op vocabulary, the same the human director uses |
| `run_op` | Stage ONE op on the draft: `set_scene_fields` (any scene field, schema-validated, $0), `set_visual`, `swap_voice`, `replan_scenes` ($0), `rewrite_scene` / `tighten_hook` (LLM, charged like a chat turn) |
| `direct` | A natural-language instruction to the project's Director — the same chat turn a human types; the planner picks the ops |
| `commit` | Persist the staged draft as a new version (`force=true` overrides a blocking coherence gate) |
| `request_upload` → `complete_upload` | Bring the creator's OWN material (image or video, ≤200 MB): a short-lived direct-upload ticket, then registration in the personal library by content identity, declaring what it shows ($0) |
| `list_material` | The creator's own material already in the library ($0) |
| `assign_material` | Put a library asset on ONE scene (with a `clip` range for video) — the same act as the Studio's «Mi Media»; then `produce` ($0) |

Premium formats (`requiresSpendConfirmation` in `list_referents`) also
need `production.confirm_spend=true` — the same gate a human passes.
Publishing to a channel is always a human decision.

Machine-readable discovery (no auth):
[`/llms.txt`](https://vixdeo.com/llms.txt) (agent front door) and
[`/.well-known/mcp.json`](https://vixdeo.com/.well-known/mcp.json)
(server descriptor). There is no SSE transport and no `/mcp` or `/sse`
path: those respond `404` with a pointer to `/v1/mcp`.

---

## 8. Where next

- **Interactive docs:** `https://vixdeo.com/v1/docs` (Swagger UI),
  `https://vixdeo.com/v1/redoc` (ReDoc), or the OpenAPI portal at
  `/developers/api-docs` in the Vixdeo console.
- **Spec download:** `https://vixdeo.com/v1/openapi.json` — drop
  it into Postman, Insomnia, or your generator of choice.
- **VBP schema reference:** see the snapshot at
  [`core/vbp_schema_v1.py`](https://github.com/draxork/vixdeo/blob/main/core/vbp_schema_v1.py).
- **Idempotency / webhooks deep-dive:** the API description at the
  top of the Swagger page covers the contract and signature scheme
  in detail.
