# Job finished

- **Method:** `POST`
- **Webhook:** `job`
- **Operation ID:** `jobWebhook`
- **Tags:** Webhooks

Sent to every enabled endpoint when a job made with an API key succeeds, fails or is cancelled. Answer with a 2xx status within 10 seconds.

## Effective servers

- `https://api.vcp.name`

## Authentication

No authentication required.

## Header parameters

- **`webhook-id` (required)**: `string`

  The delivery id. It stays the same on every attempt, so use it to skip events you already handled.
- **`webhook-timestamp` (required)**: `string`

  When this attempt was sent, in Unix seconds.
- **`webhook-signature` (required)**: `string`

  v1, a comma, then the base64 HMAC-SHA256 of webhook-id, webhook-timestamp and the body joined with dots. The key is the part of your secret after whsec\_, decoded from base64.

## Request body

**Required:** `true`

**Content type:** `application/json`

schema: `WebhookEvent`, no additional properties

The JSON body of every webhook request.

- **`api_key_id` (required)**: `string | null`

  The key that made the request, or null for webhook.test.
- **`data` (required)**

  The job, the same as GET /v1/jobs/{id} returns it. For webhook.test, the endpoint id.

  **Any of:**
  - `object`, schema: `Job`, no additional properties
    - **`created_at` (required)**: `string`, format: `date-time`

      When the job was created.
    - **`error` (required)**: `object | null`, schema: `ErrorDetail`, no additional properties

      Why the job failed, or null.
      - **`code` (required)**: `string`

        A code from the error tables.
      - **`message` (required)**: `string`

        What went wrong.
      - **`param`**: `string`

        The request field at fault, when there is one.
    - **`finished_at` (required)**: `string | null`, format: `date-time`

      When the job finished, or null while it runs.
    - **`id` (required)**: `string`

      Job id.
    - **`progress` (required)**: `number | null`, minimum: `0`, maximum: `1`

      From 0 to 1 while an avatar video renders, a dub runs or a video is cut, otherwise null.
    - **`resource_id` (required)**: `string | null`

      The voice id for voice clones, the project id for dubs and cuts, and the video id for avatar videos.
    - **`result` (required)**

      The result of a succeeded job, or null. Speech, voice designs and the voice changer give AudioResult, kept for 24 hours after the job finishes, then this is null. Transcriptions give TranscriptionResult. Voice clones give VoiceCloneResult. Avatar videos give AvatarVideoResult, dubs give DubResult and cuts give CutResult, all kept for 24 hours after the job was created.

      **Any of:**
      - `object`, schema: `AudioResult`, no additional properties

        Links to the audio. The audio is kept for 24 hours after the job finishes.
        - **`audio_url` (required)**: `string`, format: `uri`

          WAV audio link for playback.
        - **`download_url` (required)**: `string`, format: `uri`

          The same audio, served as a download.
        - **`expires_at` (required)**: `string`, format: `date-time`

          When both links stop working, 24 hours after the job finished.
      - `object`, schema: `TranscriptionResult`, no additional properties

        The transcript of the audio.
        - **`language` (required)**: `string | null`

          The spoken language, or null if unknown.
        - **`text` (required)**: `string`

          The full transcript.
        - **`words` (required)**: `array | null`

          Each word with its timing, or null without timestamps.

          **Items:**

          schema: `Word`, no additional properties
          - **`end` (required)**: `number`, minimum: `0`

            Seconds from the start of the audio.
          - **`speaker` (required)**: `string`

            Who said the word, as SPEAKER\_00, SPEAKER\_01 and so on.
          - **`start` (required)**: `number`, minimum: `0`

            Seconds from the start of the audio.
          - **`text` (required)**: `string`
      - `object`, schema: `VoiceCloneResult`, no additional properties

        The saved voice.
        - **`transcript` (required)**: `string | null`

          What the clip says, or null if it has no transcript.
        - **`voice` (required)**: `object`, schema: `Voice`, no additional properties
          - **`created_at` (required)**: `string | null`, format: `date-time`

            When the voice was saved, or null for presets.
          - **`duration_seconds` (required)**: `number | null`, minimum: `0`

            Length of the reference clip, or null.
          - **`id` (required)**: `string`

            A uuid for saved voices, or the preset id.
          - **`model` (required)**: `string | null`

            The model a preset belongs to, or null for saved voices.
          - **`name` (required)**: `string`
          - **`type` (required)**: `string`, possible values: `"clone", "design", "preset"`
      - `object`, schema: `AvatarVideoResult`, no additional properties

        Links to the video. The video is kept for 24 hours after the job was created.
        - **`download_url` (required)**: `string`, format: `uri`

          The same video, served as a download.
        - **`duration_s` (required)**: `number`, minimum: `0`

          The video length in seconds.
        - **`expires_at` (required)**: `string`, format: `date-time`

          When the links stop working and the video is deleted, 24 hours after the job was created.
        - **`height` (required)**: `integer | null`, minimum: `1`

          The video height in pixels, or null if unknown.
        - **`thumbnail_url` (required)**: `string | null`, format: `uri`

          A still image of the video, or null if there is none.
        - **`video_url` (required)**: `string`, format: `uri`

          MP4 video link for playback.
        - **`width` (required)**: `integer | null`, minimum: `1`

          The video width in pixels, or null if unknown.
      - `object`, schema: `DubResult`, no additional properties

        Links to the dubbed video and its parts. The dub is kept for 24 hours after the job was created.
        - **`audio_url` (required)**: `string`, format: `uri`

          The new voices on their own, as a WAV file.
        - **`background_url` (required)**: `string | null`, format: `uri`

          The music and sound effects without the voices, as a WAV file, or null when keep\_background was false.
        - **`download_url` (required)**: `string`, format: `uri`

          The same video, served as a download.
        - **`duration_s` (required)**: `number`, minimum: `0`

          The video length in seconds.
        - **`expires_at` (required)**: `string`, format: `date-time`

          When the links stop working and the dub is deleted, 24 hours after the job was created.
        - **`lipsynced` (required)**: `boolean`

          True when the lips were matched to the new voices.
        - **`source_language` (required)**: `string | null`

          The spoken language of the original video, or null if unknown.
        - **`speakers` (required)**: `integer`, minimum: `0`

          How many speakers were found.
        - **`subtitles_url` (required)**: `string`, format: `uri`

          GET /v1/dubs/{id}/subtitles for this job. Send your API key with it.
        - **`target_language` (required)**: `string`

          The language the video was dubbed into.
        - **`video_url` (required)**: `string`, format: `uri`

          MP4 video link for playback. The lip-synced video when lip sync ran, otherwise the dubbed video.
      - `object`, schema: `CutResult`, no additional properties

        Links to the cut video and what was taken out. The cut is kept for 24 hours after the job was created.
        - **`download_url` (required)**: `string`, format: `uri`

          The same video, served as a download.
        - **`duration_s` (required)**: `number`, minimum: `0`

          The cut video length in seconds.
        - **`expires_at` (required)**: `string`, format: `date-time`

          When the links stop working and the cut is deleted, 24 hours after the job was created.
        - **`height` (required)**: `integer | null`, minimum: `1`

          The video height in pixels, or null if unknown.
        - **`incomplete` (required)**: `array`

          Which of fillers, retakes or silences could not check the whole video. Empty when every step checked it all.

          **Items:**

          `string`, possible values: `"fillers", "retakes", "silences"`
        - **`language` (required)**: `string | null`

          The spoken language, or null if unknown.
        - **`removed` (required)**: `array`

          Each part taken out of the uploaded video, in order, with why it was taken out. Empty when nothing needed cutting.

          **Items:**

          no additional properties
          - **`end` (required)**: `number`, minimum: `0`

            Seconds from the start of the uploaded video.
          - **`reasons` (required)**: `array`

            Why the part was taken out. Lists silence only when no filler or retake was found in the part.

            **Items:**

            `string`, possible values: `"filler", "retake", "silence"`
          - **`start` (required)**: `number`, minimum: `0`

            Seconds from the start of the uploaded video.
        - **`source_duration_s` (required)**: `number`, minimum: `0`

          The uploaded video length in seconds.
        - **`video_url` (required)**: `string`, format: `uri`

          MP4 video link for playback.
        - **`width` (required)**: `integer | null`, minimum: `1`

          The video width in pixels, or null if unknown.
      - `null`
    - **`status` (required)**: `string`, possible values: `"queued", "running", "succeeded", "failed", "cancelled"`
    - **`tokens_charged` (required)**: `integer | null`, minimum: `0`

      Tokens billed, or null until billing has run.
    - **`type` (required)**: `string`, possible values: `"speech", "voice_design", "voice_clone", "voice_change", "transcription", "dub", "avatar_video", "cut"`
  - `object`, no additional properties
    - **`endpoint_id` (required)**: `string`

      The endpoint the test was sent to.
- **`timestamp` (required)**: `string`, format: `date-time`

  When this attempt was sent.
- **`type` (required)**: `string`, possible values: `"job.succeeded", "job.failed", "job.cancelled", "webhook.test"`

  What happened. webhook.test comes from Send test in Settings > API.

**Example:**

```json
{
  "type": "job.succeeded",
  "timestamp": "2026-10-30T00:18:01Z",
  "api_key_id": "5f0c2a8e-3b1d-4c7e-9a62-1d8f4e7b2c90",
  "data": {
    "id": "04bd0571-e2c8-4d5b-a3cc-e56f85773abf",
    "type": "cut",
    "status": "succeeded",
    "progress": null,
    "result": {
      "video_url": "https://files.example.com/cut.mp4",
      "download_url": "https://files.example.com/cut.mp4?download=cut.mp4",
      "duration_s": 281.344,
      "source_duration_s": 485.87,
      "width": 1080,
      "height": 1920,
      "removed": [
        {
          "start": 0,
          "end": 107.62,
          "reasons": [
            "retake"
          ]
        },
        {
          "start": 120.786,
          "end": 121.106,
          "reasons": [
            "filler"
          ]
        },
        {
          "start": 129.3,
          "end": 129.44,
          "reasons": [
            "silence"
          ]
        }
      ],
      "language": "English",
      "incomplete": [],
      "expires_at": "2026-10-31T00:10:42Z"
    },
    "error": null,
    "tokens_charged": 16311,
    "resource_id": "857fcb81-9073-4f26-97c7-a388979fcdca",
    "created_at": "2026-10-30T00:10:42Z",
    "finished_at": "2026-10-30T00:18:01Z"
  }
}
```

## Responses

### 200 Any 2xx status within 10 seconds marks the event delivered. Anything else, including a redirect, is sent again later.

## Schemas

- `ErrorDetail` — shown above.

### JobType

**Type:** `string`, possible values: `"speech", "voice_design", "voice_clone", "voice_change", "transcription", "dub", "avatar_video", "cut"`

### JobStatus

**Type:** `string`, possible values: `"queued", "running", "succeeded", "failed", "cancelled"`

- `Job` — shown above.

### AudioResult

**Type:** `object`, no additional properties

Links to the audio. The audio is kept for 24 hours after the job finishes.

*Schema `AudioResult` is shown above.*

**Example:**

```json
{
  "audio_url": "https://files.example.com/speech.wav",
  "download_url": "https://files.example.com/speech.wav?download=speech.wav",
  "expires_at": "2026-10-31T00:10:42Z"
}
```

### TranscriptionResult

**Type:** `object`, no additional properties

The transcript of the audio.

*Schema `TranscriptionResult` is shown above.*

**Example:**

```json
{
  "text": "Welcome back to the show.",
  "language": "English",
  "words": [
    {
      "text": "Welcome",
      "start": 0.32,
      "end": 0.71,
      "speaker": "SPEAKER_00"
    },
    {
      "text": "back",
      "start": 0.71,
      "end": 0.94,
      "speaker": "SPEAKER_00"
    }
  ]
}
```

### VoiceCloneResult

**Type:** `object`, no additional properties

The saved voice.

*Schema `VoiceCloneResult` is shown above.*

**Example:**

```json
{
  "voice": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "name": "Narrator",
    "type": "clone",
    "model": null,
    "duration_seconds": 12.4,
    "created_at": "2026-10-30T00:10:42Z"
  },
  "transcript": "Welcome back to the show."
}
```

### AvatarVideoResult

**Type:** `object`, no additional properties

Links to the video. The video is kept for 24 hours after the job was created.

*Schema `AvatarVideoResult` is shown above.*

**Example:**

```json
{
  "video_url": "https://files.example.com/avatar.mp4",
  "download_url": "https://files.example.com/avatar.mp4?download=avatar.mp4",
  "thumbnail_url": "https://files.example.com/avatar.jpg",
  "duration_s": 8.2,
  "width": 1280,
  "height": 720,
  "expires_at": "2026-10-31T00:10:42Z"
}
```

### DubResult

**Type:** `object`, no additional properties

Links to the dubbed video and its parts. The dub is kept for 24 hours after the job was created.

*Schema `DubResult` is shown above.*

**Example:**

```json
{
  "video_url": "https://files.example.com/dub.mp4",
  "download_url": "https://files.example.com/dub.mp4?download=dub.mp4",
  "audio_url": "https://files.example.com/dub-voices.wav",
  "background_url": "https://files.example.com/dub-background.wav",
  "subtitles_url": "https://api.example.com/v1/dubs/04bd0571-e2c8-4d5b-a3cc-e56f85773abf/subtitles",
  "duration_s": 94.5,
  "source_language": "English",
  "target_language": "es",
  "speakers": 2,
  "lipsynced": false,
  "expires_at": "2026-10-31T00:10:42Z"
}
```

### CutResult

**Type:** `object`, no additional properties

Links to the cut video and what was taken out. The cut is kept for 24 hours after the job was created.

*Schema `CutResult` is shown above.*

**Example:**

```json
{
  "video_url": "https://files.example.com/cut.mp4",
  "download_url": "https://files.example.com/cut.mp4?download=cut.mp4",
  "duration_s": 281.344,
  "source_duration_s": 485.87,
  "width": 1080,
  "height": 1920,
  "removed": [
    {
      "start": 0,
      "end": 107.62,
      "reasons": [
        "retake"
      ]
    },
    {
      "start": 120.786,
      "end": 121.106,
      "reasons": [
        "filler"
      ]
    },
    {
      "start": 129.3,
      "end": 129.44,
      "reasons": [
        "silence"
      ]
    }
  ],
  "language": "English",
  "incomplete": [],
  "expires_at": "2026-10-31T00:10:42Z"
}
```

- `Word` — shown above.
- `Voice` — shown above.

### WebhookEvent

**Type:** `object`, no additional properties

The JSON body of every webhook request.

*Schema `WebhookEvent` is shown above.*

**Example:**

```json
{
  "type": "job.succeeded",
  "timestamp": "2026-10-30T00:18:01Z",
  "api_key_id": "5f0c2a8e-3b1d-4c7e-9a62-1d8f4e7b2c90",
  "data": {
    "id": "04bd0571-e2c8-4d5b-a3cc-e56f85773abf",
    "type": "cut",
    "status": "succeeded",
    "progress": null,
    "result": {
      "video_url": "https://files.example.com/cut.mp4",
      "download_url": "https://files.example.com/cut.mp4?download=cut.mp4",
      "duration_s": 281.344,
      "source_duration_s": 485.87,
      "width": 1080,
      "height": 1920,
      "removed": [
        {
          "start": 0,
          "end": 107.62,
          "reasons": [
            "retake"
          ]
        },
        {
          "start": 120.786,
          "end": 121.106,
          "reasons": [
            "filler"
          ]
        },
        {
          "start": 129.3,
          "end": 129.44,
          "reasons": [
            "silence"
          ]
        }
      ],
      "language": "English",
      "incomplete": [],
      "expires_at": "2026-10-31T00:10:42Z"
    },
    "error": null,
    "tokens_charged": 16311,
    "resource_id": "857fcb81-9073-4f26-97c7-a388979fcdca",
    "created_at": "2026-10-30T00:10:42Z",
    "finished_at": "2026-10-30T00:18:01Z"
  }
}
```
