> ## Documentation Index
> Fetch the complete documentation index at: https://docs.percify.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Make a short presenter video from a topic with the API

> POST /v3/mascot/api turns a topic into a finished short video: script, AI presenter, voice, lip-sync, captions and music, delivered as one video URL.

`POST https://api.percify.io/v3/mascot/api` makes a complete short video from one request: Percify writes a spoken script from your topic, generates a presenter image and voice, lip-syncs them, then adds captions, b-roll and a music bed. It returns a `jobId` at once; poll `GET /v3/mascot/api/{jobId}` until `status` is `done` and read `videoUrl`. The job is charged as the generations it runs and fully refunded if it fails.

The same pipeline is the `make_video` tool on the [MCP server](/mcp-server).

| Method | Path                            | What it does                                                    |
| ------ | ------------------------------- | --------------------------------------------------------------- |
| `POST` | `/v3/mascot/api`                | Start a short video                                             |
| `GET`  | `/v3/mascot/api/{jobId}`        | Read the job                                                    |
| `POST` | `/v3/mascot/api/{jobId}/reedit` | Re-cut a finished job with another edit, without a new lip-sync |
| `GET`  | `/v3/mascot/api/options`        | Voices, presenter presets and script categories                 |

## Start a video

<ParamField body="topic" type="string">
  What the video is about. Long topics are cut at a sentence boundary around 900 characters. Leave it out and Percify picks an angle itself.
</ParamField>

<ParamField body="seconds" type="number" default="45">
  Target length from 20 to 180 seconds. It steers the script; the final length is however long the script takes to say.
</ParamField>

<ParamField body="gender" type="string" default="man">
  Presenter: `man` or `woman`.
</ParamField>

<ParamField body="preset" type="string" default="creator">
  Presenter look: `creator`, `newscaster` or `pixar`. Other values fall back to `creator`.
</ParamField>

<ParamField body="voice" type="string">
  A voice name for the chosen gender from `GET /v3/mascot/api/options`. Defaults are `Archer` for `man` and `Lucy` for `woman`; a name that does not match the gender uses the default.
</ParamField>

<ParamField body="flow" type="string">
  Force an edit style, such as `classic`, `reveal`, `hook-reveal`, `broll-punch`, `clean-punch` or `karaoke`. Leave it out for the default edit.
</ParamField>

<ParamField body="captions" type="boolean" default="true">
  Apply the edit and burned-in captions.
</ParamField>

<ParamField body="music" type="boolean" default="true">
  Add a background music bed.
</ParamField>

<ParamField body="idempotencyKey" type="string">
  Send the same value on retries to get the original job back instead of starting and paying for another. On this endpoint it goes in the body, not a header.
</ParamField>

<ParamField body="webhook" type="string">
  `https` URL that receives `{ jobId, status, videoUrl, creditsSpent }` when the job is done, or `{ jobId, status, error }` if it fails. See [Webhooks](/guides/webhooks).
</ParamField>

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -s -X POST https://api.percify.io/v3/mascot/api \
    -H "Authorization: Bearer $PERCIFY_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "topic": "Why short onboarding videos beat long help articles",
      "seconds": 40,
      "gender": "woman",
      "preset": "creator",
      "idempotencyKey": "onboarding-short-001"
    }'
  ```

  ```javascript Node.js theme={"system"}
  const API = "https://api.percify.io/v3/mascot/api";
  const headers = {
    Authorization: `Bearer ${process.env.PERCIFY_API_TOKEN}`,
    "Content-Type": "application/json",
  };

  let { data } = await (await fetch(API, {
    method: "POST",
    headers,
    body: JSON.stringify({
      topic: "Why short onboarding videos beat long help articles",
      seconds: 40,
      gender: "woman",
      idempotencyKey: "onboarding-short-001",
    }),
  })).json();

  while (data.status !== "done" && data.status !== "failed") {
    await new Promise((r) => setTimeout(r, 15000));
    ({ data } = await (await fetch(`${API}/${data.jobId}`, { headers })).json());
  }
  console.log(data.status, data.videoUrl, data.degraded);
  ```

  ```python Python theme={"system"}
  import os, time, requests

  API = "https://api.percify.io/v3/mascot/api"
  H = {"Authorization": f"Bearer {os.environ['PERCIFY_API_TOKEN']}"}

  data = requests.post(API, headers=H, json={
      "topic": "Why short onboarding videos beat long help articles",
      "seconds": 40,
      "gender": "woman",
      "idempotencyKey": "onboarding-short-001",
  }).json()["data"]

  while data["status"] not in ("done", "failed"):
      time.sleep(15)
      data = requests.get(f"{API}/{data['jobId']}", headers=H).json()["data"]
  print(data["status"], data["videoUrl"], data["degraded"])
  ```
</CodeGroup>

```json 201 Response theme={"system"}
{ "success": true, "data": { "jobId": "7a1e4c2b-0d9f-4b8e-a3c5-5f2e9d1b6a70", "status": "scripting" } }
```

## Read a job

`GET /v3/mascot/api/{jobId}`

<ResponseField name="data.status" type="string">
  `scripting`, `portrait`, `animating`, `captioning`, then `done` or `failed`. `stage` repeats it.
</ResponseField>

<ResponseField name="data.progress" type="number | null">
  From 0 to 1 as the stages advance.
</ResponseField>

<ResponseField name="data.script" type="string | null">
  The script the presenter speaks.
</ResponseField>

<ResponseField name="data.portraitUrl" type="string | null">
  The presenter image, available during the job.
</ResponseField>

<ResponseField name="data.rawVideoUrl" type="string | null">
  The lip-synced talking video before editing.
</ResponseField>

<ResponseField name="data.videoUrl" type="string | null">
  The finished, edited video.
</ResponseField>

<ResponseField name="data.creditsSpent" type="integer | null">
  Credits the job used.
</ResponseField>

<ResponseField name="data.applied" type="object | null">
  What the edit delivered: `captions`, `broll`, `images`, `music` and `loudnorm`, plus `skipped`, which names any step that fell back and why.
</ResponseField>

<ResponseField name="data.degraded" type="boolean | null">
  `true` when a step was skipped, so the video is plainer than requested.
</ResponseField>

The object also has `jobId`, `flow`, `topic`, `voice`, `seconds`, `posterUrl`, `error`, `createdAt` and `updatedAt`.

<Warning>
  `done` means the pipeline reached the end. If captions, b-roll, images or music fail, the job still finishes with a plainer video. Check `degraded` and `applied` before you publish it as fully edited.
</Warning>

## Re-cut a finished video

`POST /v3/mascot/api/{jobId}/reedit` takes `flow`, `captions` and `music`, reuses the job's script, voice and lip-sync, and returns a new `jobId` to poll the same way. It only works on your own jobs that already have a lip-sync.

## Errors

| Status | Message                              | Cause                                    |
| ------ | ------------------------------------ | ---------------------------------------- |
| `400`  | `Invalid jobId`                      | The id is not a UUID.                    |
| `400`  | `webhook must use https` and similar | Fix the webhook URL.                     |
| `404`  | `Job not found`                      | The job does not belong to your account. |

A job that runs out of credits partway fails with `status: "failed"`; the error says how many credits it needed, and the credits it used are refunded.

## Related

<CardGroup cols={2}>
  <Card title="Async jobs and polling" href="/api-reference/async-jobs">
    Statuses for every job type.
  </Card>

  <Card title="Talking avatar pipeline" href="/api-reference/avatars/overview">
    Build your own presenter video step by step.
  </Card>

  <Card title="MCP server" href="/mcp-server">
    make\_video, reedit\_video and get\_mascot for agents.
  </Card>

  <Card title="Webhooks" href="/guides/webhooks">
    Get the finished video pushed to you.
  </Card>
</CardGroup>
