# AutoPersonify Developer API (v1)

Make finished AI presenter videos from your own software. Choose one of AutoPersonify's presenters or your own digital twin, send a script, get a video.

- Base URL: https://autopersonify.com/api/v1
- Auth: `Authorization: Bearer ap_live_…` (create keys in AutoPersonify → Integrations → Developer API)
- Format: JSON in, JSON out. Timestamps are ISO 8601 (UTC).
- Call from server code only. Never ship the key to a browser or mobile app.
- Twins are created only inside the AutoPersonify app (they need a guided, consented recording). The API can use ready twins but cannot build them.
- Videos use the account's normal plan allowance and also appear in the user's Projects.

## Quick start

```bash
curl https://autopersonify.com/api/v1/presenters -H "Authorization: Bearer $AUTOPERSONIFY_API_KEY"

curl -X POST https://autopersonify.com/api/v1/videos \
  -H "Authorization: Bearer $AUTOPERSONIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"presenter_id":"<id from /presenters>","script":"Hi Sarah, here is your weekly PPC summary...","idempotency_key":"report-2026-09-26-client-42"}'

curl https://autopersonify.com/api/v1/videos/<video id> -H "Authorization: Bearer $AUTOPERSONIFY_API_KEY"
```

## Endpoints

| Method | Path | Purpose |
|---|---|---|
| GET | /me | Account, plan and remaining video seconds |
| GET | /presenters?q= | Presenters available to the API: id, name, gender, language, thumbnail_url, preview_video_url |
| GET | /twins | The account's twins: id, name, status (ready/building), usable_in_videos, thumbnail_url, preview_video_url, voice_name |
| GET | /voices | The user's own starred voices (optional voice_id for videos) |
| POST | /videos | Create a video (returns 202 with status "rendering") |
| GET | /videos/{id} | Status, progress (0–100), video_url when ready, error when failed |
| GET | /videos?limit=20&before= | Recent videos, newest first |
| DELETE | /videos/{id} | Delete a video |

### POST /videos body

| Field | Type | Notes |
|---|---|---|
| script | string, required | Exactly what the presenter says. Max 3,000 characters by default (≈3 minutes). |
| presenter_id | string | From GET /presenters. Provide this **or** twin_id. |
| twin_id | string | From GET /twins; must have usable_in_videos=true. Uses the twin's saved voice. |
| voice_id | string, optional | From GET /voices, to speak in one of the user's own voices. |
| title | string, optional | Shown in the user's Projects. |
| webhook_url | https URL, optional | Overrides the account webhook for this video. |
| idempotency_key | string, optional | Same key = same video, never charged twice. Also accepted as an Idempotency-Key header. Strongly recommended. |

### Video object

```json
{ "id": "…", "status": "queued | rendering | ready | failed", "progress": 42,
  "presenter": "Anna", "duration_seconds": 38.2, "thumbnail_url": null,
  "video_url": "https://… (signed, valid 1 hour — fetch GET /videos/{id} again for a fresh one)",
  "error": null }
```

Videos usually take 2–10 minutes. Poll every 10–15 seconds, or use webhooks. Don't store video_url long-term; store the video id.

## Webhooks

Set an account webhook in Integrations → Developer API (you get a signing secret once) or pass webhook_url per video. We POST:

```json
{ "event": "video.ready" | "video.failed", "data": { …video object… }, "sent_at": "…" }
```

Headers: `X-AutoPersonify-Timestamp` and `X-AutoPersonify-Signature: sha256=<hex>` where the hex is HMAC-SHA256(secret, timestamp + "." + raw body). Verify before trusting, and reject timestamps older than 5 minutes.

```js
import crypto from "node:crypto";
const expected = "sha256=" + crypto.createHmac("sha256", process.env.AUTOPERSONIFY_WEBHOOK_SECRET)
  .update(req.headers["x-autopersonify-timestamp"] + "." + rawBody).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers["x-autopersonify-signature"]));
```

## Errors — what to show the user

Every error looks like:

```json
{ "error": { "code": "allowance_exceeded", "message": "…", "user_message": "You've used all your video minutes…",
  "action": "upgrade", "retryable": false, "upgrade_url": "https://autopersonify.com/app/billing" } }
```

Always display `user_message` to the end user (it is written for them). Use `action` to decide what the UI does. Only retry when `retryable` is true.

| code | HTTP | Meaning | What your app should do |
|---|---|---|---|
| invalid_api_key | 401 | The key is missing, mistyped or unknown. | Ask the user for a new key (AutoPersonify → Integrations → Developer API). Don't retry. |
| key_revoked | 401 | The key was revoked. | Ask the user to create a new key. Don't retry. |
| allowance_exceeded | 402 | The account has used its video minutes. | Show user_message and a button to upgrade_url. Don't retry until they upgrade. |
| plan_required | 402 | A paid plan is needed (for example using a free-plan twin in a new video). | Show user_message and link to upgrade_url. |
| twin_not_ready | 409 | The twin is still building. | Tell the user and refresh the picker later. |
| not_found | 404 | Presenter, twin or video doesn't exist (or was deleted). | Refresh the picker list and ask the user to choose again. |
| invalid_request | 400 | Missing or invalid fields; see error.fields. | Fix the request. Show field messages if the user typed them. |
| script_too_long | 400 | Script exceeds the per-video character limit. | Ask the user to shorten or split the script. |
| rate_limited | 429 | More than 60 requests or 10 video creates per minute. | Wait for the Retry-After header, then retry. |
| concurrency_limit | 429 | Too many videos rendering at once. | Wait for one to finish (Retry-After), then retry. |
| daily_limit | 429 | Daily video limit reached. | Tell the user to try again tomorrow. Don't retry today. |
| render_failed | 500 | The video couldn't be made (also appears as video.error when status is failed). | Show user_message. Offer one manual retry; if it fails again suggest contacting support. |
| service_unavailable | 503 | Temporary problem on our side. | Retry with exponential backoff (e.g. 5s, 20s, 60s), at most 3 times, then show user_message. |

When a video fails after it was accepted, GET /videos/{id} returns status "failed" with `error.user_message`. Show it, keep the original script so the user can retry with one click, and don't loop retries automatically.

## Limits

60 requests/minute per key, 10 video creates/minute, plan-based concurrent and daily video limits, video minutes per your plan (Free 3 minutes, Starter £49, Growth £149, Scale £499 per month).

## Security checklist

- Keep the key in a server-side secret; rotate by creating a new key and revoking the old one.
- Keys are stored hashed by AutoPersonify and shown only once.
- Verify webhook signatures.
- Don't log full keys or signed video URLs.

## Prompt for AI coding agents (Lovable, Cursor, Claude Code, etc.)

```text
Integrate my AutoPersonify account so I can make AI presenter videos (with a stock presenter or my own digital twin) from this app.

1. Read the docs at https://autopersonify.com/developers.md (OpenAPI: https://autopersonify.com/openapi.json).
2. Ask me for my AutoPersonify API key and store it as a server-side secret named AUTOPERSONIFY_API_KEY. Never expose it to the browser.
3. Make all calls from server code to https://autopersonify.com/api/v1 with the header Authorization: Bearer <key>.
4. Build a page where I pick a presenter (GET /presenters, show thumbnail + preview video) or one of my twins (GET /twins, only those with usable_in_videos=true), type or generate a script, and create the video (POST /videos with an idempotency_key).
5. Track status by polling GET /videos/{id} every 10–15 seconds (or via webhook) and show the finished video_url in a player.
6. Handle every error exactly as the docs' error table says — always show error.user_message, and show an Upgrade button linking to error.upgrade_url when error.action is "upgrade".
```
