> For the complete documentation index, see [llms.txt](https://bountyv.gitbook.io/vdocs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://bountyv.gitbook.io/vdocs/clip-api/clip-stitch.md).

# Clip Stitch

Upload a hook and your own audio, pick an end from our Vault library, get a finished reel.

```
POST /api/v1/clip/stitch
```

Asynchronous. Upload a start "hook" clip, choose an **end** from our Vault viral library, and optionally lay a track over the whole thing — either **your own audio file** (`audio`) or a Vault track (`audio_id`). We normalize both clips to 1080×1920 / 30fps and stitch them with a transition. The call returns a `job_id`; poll [Job status](/vdocs/video-api/jobs.md) (`GET /api/v1/jobs/{job_id}`) until `status` is `completed`, then download the reel from `GET /api/v1/jobs/{job_id}/video`.

Call [`GET /api/v1/clip/library`](/vdocs/clip-api/clip-overview.md#clip-library) first to get the available end clips (`ends`) and audio tracks (`audio`), and pass their `id` values as `end_id` / `audio_id`.

> **Bring your own audio.** Uploading `audio` is the recommended path. You keep control of exactly which track goes over the reel, and you're responsible for holding the rights to it — so make sure you're licensed for the track you send. If both `audio` and `audio_id` are given, the uploaded file wins and `audio_id` is ignored.

## Authentication

Standard header:

```
Authorization: Bearer sk_live_...
```

See [Authentication](/vdocs/authentication.md) for details. Requires the `clip` product on your account.

## Request

**Content-Type:** `multipart/form-data`

| Field                 | Type   | Required | Default     | Description                                                                                                                                       |
| --------------------- | ------ | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hook`                | file   | ✅        | —           | The start clip (your hook). Max 200MB.                                                                                                            |
| `end_id`              | string | ✅        | —           | A Vault viral id from `GET /api/v1/clip/library`.                                                                                                 |
| `audio`               | file   | —        | —           | Your own audio track to lay over the stitch. Max 50MB. Takes precedence over `audio_id`.                                                          |
| `audio_id`            | string | —        | —           | A Vault audio id to lay over the stitch. Ignored if `audio` is sent. Omit both to keep the clips' own audio.                                      |
| `transition`          | string | —        | `crossfade` | One of `cut`, `fade`, `crossfade`.                                                                                                                |
| `transition_duration` | number | —        | `0.6`       | Seconds, clamped to `0`–`3`.                                                                                                                      |
| `audio_mode`          | string | —        | see note    | One of `keep`, `mute`, `start-only`, `end-only`, `replace`. Defaults to `replace` when a track is supplied (uploaded or Vault), otherwise `keep`. |

`end_id` must reference a Vault item that is a `video` in the `virals` category; `audio_id`, if given, must reference a Vault item of type `audio`. Setting `audio_mode=replace` requires either an `audio` file or an `audio_id`.

## Response

```json
{
  "job_id": "V1StGXR8_Z5jdHi6B-myT",
  "status": "queued",
  "charged_cents": 15,
  "balance_cents": 4985,
  "poll_url": "/api/v1/jobs/V1StGXR8_Z5jdHi6B-myT"
}
```

HTTP status: `202 Accepted`.

Poll [Job status](/vdocs/video-api/jobs.md) every 5–10 seconds until `status` is `completed` or `failed`. Once completed, `video_url` on that response points at `GET /api/v1/jobs/{job_id}/video`, which streams the finished MP4 through our domain.

## Example

```bash
# 1. List the library
curl https://substance-api.com/api/v1/clip/library \
  -H "Authorization: Bearer $SUBSTANCE_API_KEY"

# 2. Stitch your hook onto a chosen end, with your own track over it
curl -X POST https://substance-api.com/api/v1/clip/stitch \
  -H "Authorization: Bearer $SUBSTANCE_API_KEY" \
  -F "hook=@./hook.mp4" \
  -F "end_id=VAULT_VIRAL_ID" \
  -F "audio=@./my-track.mp3" \
  -F "transition=crossfade"
```

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

const form = new FormData();
form.set("hook", new Blob([fs.readFileSync("hook.mp4")]), "hook.mp4");
form.set("end_id", "VAULT_VIRAL_ID");
form.set("audio", new Blob([fs.readFileSync("my-track.mp3")]), "my-track.mp3");

const res = await fetch("https://substance-api.com/api/v1/clip/stitch", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SUBSTANCE_API_KEY}` },
  body: form,
});
const { job_id } = await res.json();
// poll /api/v1/jobs/{job_id} until status === "completed",
// then GET /api/v1/jobs/{job_id}/video
```

## Errors

| Status | When                                                                                                                             |
| ------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Missing `hook` / `end_id`, `hook` over 200MB, `audio` over 50MB, bad `end_id`/`audio_id`, or `audio_mode=replace` with no track. |
| `401`  | Missing/invalid API key.                                                                                                         |
| `402`  | Insufficient balance.                                                                                                            |
| `403`  | Your account doesn't have the `clip` product enabled.                                                                            |
| `502`  | The stitch couldn't be started. Balance is refunded — see `refunded_cents`.                                                      |
| `503`  | Pipeline temporarily offline.                                                                                                    |

A job can also complete with `status: "failed"` when polled — this is refunded automatically. See [Errors](/vdocs/errors.md) for full details.

## Pricing

Billed per call — see [Credits & pricing](/vdocs/account/credits.md) for current rates.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://bountyv.gitbook.io/vdocs/clip-api/clip-stitch.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
