> ## 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.

# Lip-sync a photo to audio with the Percify API

> Turn a portrait and an audio file into a talking avatar video with POST /v1/run and infinitetalk-fast or infinitetalk. Inputs, pricing and examples.

To make a photo talk through the Percify API, call `POST /v1/run` with `modelId` set to `infinitetalk-fast` or `infinitetalk`, an `image` URL of a face and an `audio` URL of the speech. Percify returns a generation id, and the finished lip-synced video appears in `output.urls` when the generation succeeds. It is billed per second of audio.

```http theme={"system"}
POST https://api.percify.io/v3/playground/v1/run
```

## Which model should I use?

| Model id            | Resolution                        | Best for                                             |
| ------------------- | --------------------------------- | ---------------------------------------------------- |
| `infinitetalk-fast` | 480p                              | Drafts, tests and high volume                        |
| `infinitetalk`      | `480p` or `720p` (default `720p`) | Final renders. `720p` costs twice as much as `480p`. |

## Body

<ParamField body="modelId" type="string" required>
  `infinitetalk-fast` or `infinitetalk`.
</ParamField>

<ParamField body="input.image" type="string" required>
  Public `https` URL of the face to animate, such as a `.jpg` or `.png` portrait with a clearly visible face. The video keeps this image's aspect ratio.
</ParamField>

<ParamField body="input.audio" type="string" required>
  Public `https` URL of the speech, as `.mp3` or `.wav`. Percify reads the file's length to price the run, so it must be reachable.
</ParamField>

<ParamField body="input.resolution" type="string" default="720p">
  `infinitetalk` only: `480p` or `720p`. `infinitetalk-fast` ignores it.
</ParamField>

<ParamField body="input.seed" type="integer" default="-1">
  Random seed. `-1` picks a random one.
</ParamField>

<ParamField body="webhook" type="string">
  Optional `https` URL to receive the result. See [Webhooks](/guides/webhooks).
</ParamField>

Send an `Idempotency-Key` header too, so a retry never charges twice.

## Example

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -s -X POST https://api.percify.io/v3/playground/v1/run \
    -H "Authorization: Bearer $PERCIFY_API_TOKEN" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: intro-video-v1" \
    -d '{
      "modelId": "infinitetalk",
      "input": {
        "image": "https://example.com/presenter.jpg",
        "audio": "https://example.com/intro.mp3",
        "resolution": "720p"
      }
    }'
  ```

  ```javascript Node.js theme={"system"}
  const res = await fetch("https://api.percify.io/v3/playground/v1/run", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PERCIFY_API_TOKEN}`,
      "Content-Type": "application/json",
      "Idempotency-Key": "intro-video-v1",
    },
    body: JSON.stringify({
      modelId: "infinitetalk",
      input: {
        image: "https://example.com/presenter.jpg",
        audio: "https://example.com/intro.mp3",
        resolution: "720p",
      },
    }),
  });
  const { data } = await res.json();
  console.log(data.id, data.creditsSpent);
  ```

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

  data = requests.post(
      "https://api.percify.io/v3/playground/v1/run",
      headers={
          "Authorization": f"Bearer {os.environ['PERCIFY_API_TOKEN']}",
          "Idempotency-Key": "intro-video-v1",
      },
      json={
          "modelId": "infinitetalk",
          "input": {
              "image": "https://example.com/presenter.jpg",
              "audio": "https://example.com/intro.mp3",
              "resolution": "720p",
          },
      },
  ).json()["data"]
  print(data["id"], data["creditsSpent"])
  ```
</CodeGroup>

Then wait for it with [`GET /v1/generations/{id}?wait=45`](/api-reference/generations/get). When it succeeds:

```json Succeeded (trimmed) theme={"system"}
{
  "success": true,
  "data": {
    "id": "b2d7e0f4-5c1a-4e93-8f26-0a9c3d7e1b45",
    "modelId": "infinitetalk",
    "status": "succeeded",
    "output": {
      "type": "video",
      "urls": ["https://cdn.percify.io/media-assets/playground/b2d7e0f4-5c1a-4e93-8f26-0a9c3d7e1b45-0"]
    },
    "error": null,
    "mediaDurationSec": 10.4
  }
}
```

`mediaDurationSec` is the audio length Percify measured and billed, and `creditsSpent` holds the credits charged.

## How much does lip-sync cost?

Both models are billed per second of the audio you send, and `infinitetalk` at `720p` costs twice its `480p` price. For the exact credits, send the same body to [`POST /v1/estimate`](/api-reference/generations/estimate) first; it measures your audio file and never charges. Credits are refunded if the run fails.

## Errors specific to lip-sync

| Message starts with                        | Fix                                              |
| ------------------------------------------ | ------------------------------------------------ |
| `Missing required field: image` or `audio` | Send both URLs.                                  |
| `audio URL is not reachable`               | Make the file public and use `https`.            |
| `Unsupported media format`                 | Convert the audio to `.mp3` or `.wav`.           |
| `Image rejected by content moderation`     | Use a different portrait. Nothing was charged.   |
| `resolution must be one of: 480p, 720p`    | Use one of those values, on `infinitetalk` only. |

Long clips take longer to render. A generation fails and refunds if it is still running after 15 minutes plus 5 seconds per second of audio, or after 30 minutes at most. See [Async jobs](/api-reference/async-jobs).

## Related

<CardGroup cols={2}>
  <Card title="Talking avatar pipeline" href="/api-reference/avatars/overview">
    Generate the face and the voice too.
  </Card>

  <Card title="Voice cloning and speech" href="/api-reference/audio/overview">
    Make the audio track with zonos2.
  </Card>

  <Card title="Get a generation" href="/api-reference/generations/get">
    Wait for the video URL.
  </Card>

  <Card title="Lip-sync troubleshooting" href="/troubleshooting/lip-sync-errors">
    When the mouth does not match the audio.
  </Card>
</CardGroup>
