Your first edit in five minutes
The fastest path is a prebuilt tool. It gives your agent a narrow, stable contract for one common media task.
curl -X POST https://kinopipe.com/api/v1/tools/resize-video \
-H "Authorization: Bearer $KINOPIPE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: resize-demo-001" \
-d '{
"inputs": [{ "id": "main", "url": "https://example.com/source.mp4" }],
"options": {
"aspect_ratio": "9:16",
"fit": "cover"
}
}'Bearer keys for server-side requests
KinoPipe API keys begin with kp_live_. Keep them on your server and never expose them in browser code or public agent prompts.
The raw key is returned only once. KinoPipe stores a SHA-256 hash, so a lost key cannot be recovered.
Send files without hosting them first
Ask KinoPipe for a signed storage policy, submit its fields with the file, then pass the returned mediaUrl to a job or tool.
/api/v1/uploadsconst file = document.querySelector("input[type=file]").files[0]
const signed = await fetch("https://kinopipe.com/api/v1/uploads", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.KINOPIPE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
filename: "source.mp4",
contentType: "video/mp4",
byteSize: file.size,
}),
}).then((response) => response.json())
const body = new FormData()
for (const [name, value] of Object.entries(signed.upload.fields)) {
body.append(name, value)
}
body.append("file", file)
await fetch(signed.upload.uploadUrl, { method: "POST", body })
// Use this URL in inputs[].url within 24 hours.
console.log(signed.upload.mediaUrl)Your browser or server sends the file to the signed storage URL. KinoPipe deletes direct uploads after one day.
Give an agent every video tool at once
Connect the KinoPipe remote MCP server once. Your agent discovers focused, typed video tools and can queue edits, poll jobs and retrieve downloadable results without generating FFmpeg commands.
https://kinopipe.com/mcpAgent configuration
Add the URL to an OAuth-capable MCP client. KinoPipe opens a secure sign-in and consent screen automatically—no API key to paste. Bearer API keys remain supported for clients that need manual headers.
Connect to ChatGPT, Claude or Codex{
"mcpServers": {
"kinopipe": {
"url": "https://kinopipe.com/mcp"
}
}
}Editing tools return immediately with a jobId. The agent calls get_job until the edit succeeds, then receives the real temporary download URL.
| Capability | Examples | Result |
|---|---|---|
| Focused tools | trim_video · resize_video · add_music_to_video | Typed arguments and useful defaults for common edits. |
| Custom recipes | create_video_job | Composable bounded operations for advanced jobs. |
| Job retrieval | get_job | Progress, errors, credits and downloadable outputs. |
Compose several edits in one job
Use the Jobs API when an agent needs a pipeline. KinoPipe accepts the job immediately with HTTP 202, then processes the operations in order on a media worker.
/api/v1/jobs{
"name": "social-cut",
"inputs": [{
"id": "main",
"url": "https://example.com/source.mp4",
"filename": "source.mp4"
}],
"operations": [
{ "type": "trim", "start": 2, "end": 17 },
{
"type": "resize",
"width": 1080,
"height": 1920,
"fit": "cover"
}
],
"output": { "format": "mp4", "quality": "balanced" }
}Read job status
Poll the returned statusUrl with the same API key until the status is succeeded or failed.
/api/v1/jobs/{jobId}{
"ok": true,
"job": {
"id": "f7541d5e-…",
"status": "succeeded",
"progress": 100,
"consumedCredits": 3
},
"result": {
"output": {
"downloadUrl": "https://cdn.kinopipe.com/…",
"contentType": "video/mp4",
"byteSize": 437021,
"filename": "output.mp4"
},
"outputs": [{
"downloadUrl": "https://cdn.kinopipe.com/…",
"contentType": "video/mp4",
"byteSize": 437021,
"filename": "output.mp4"
}],
"runtimeMs": 2501
}
}Supported operations
| Type | Fields | Purpose |
|---|---|---|
| trim | start, end? | Cut a time range in seconds. |
| resize | width, height, fit? | Scale and cover or contain the output frame. |
| thumbnail | — | Return one JPG or WebP frame. |
| subtitles | url or input | Burn a public SRT or WebVTT file into the video. |
| crop | aspectRatio, focus | Remove edges to match a social aspect ratio. |
| gif | fps, width, maxColors | Generate a palette-optimized animated GIF. |
| remove_audio | — | Remove every audio stream from a video output. |
| add_audio | input, mode, volumes, loop | Mix or replace the soundtrack with a named audio input. |
| concat | inputs, width, height | Normalize and join two to six clips. |
| watermark | input, position, width, opacity | Overlay a named image input. |
| picture_in_picture | input, position, width | Overlay a named video input. |
| split | segmentDuration, maxSegments | Return up to 50 independently playable MP4 files. |
Send a stable Idempotency-Key when retrying the same edit. A repeated key returns the existing job with HTTP 200 and does not run or bill it twice.
Focused endpoints with useful defaults
Every tool accepts named inputs, options and an optional webhook. The same presets can be tested by a human on their public tool pages.
/api/v1/tools/{slug}| Tool | Options | Open |
|---|---|---|
compress-videoCompress video | quality: fast | balanced | quality | Try |
video-to-mp4Video to MP4 | No options required | Try |
resize-videoResize video | aspect_ratio: 9:16 | 1:1 | 16:9 · fit: cover | contain | Try |
extract-audioExtract audio | format: mp3 | wav | Try |
video-thumbnail-generatorGenerate thumbnails | timestamp: number · format: jpg | webp | Try |
add-subtitles-to-videoBurn subtitles | inputs: main video + captions (SRT or WebVTT) | Try |
trim-videoTrim video | start: number · end: number | Try |
optimize-video-for-webOptimize for web | quality: fast | balanced | quality | Try |
merge-videosMerge videos | inputs: 2–6 videos · aspect_ratio: 9:16 | 1:1 | 16:9 | Try |
add-watermark-to-videoAdd watermark | inputs: video + watermark · position · width · opacity | Try |
picture-in-picture-videoPicture in picture | inputs: main + pip video · position · width | Try |
create-social-video-clipSocial video clip | start · end · aspect_ratio · fit | Try |
add-music-to-videoAdd music | inputs: main + music · mode · music_volume · source_volume | Try |
convert-video-to-gifVideo to GIF | start · duration (max 30s) · fps · width | Try |
crop-videoCrop video | aspect_ratio: 9:16 | 1:1 | 4:5 | 16:9 · focus | Try |
remove-audio-from-videoRemove audio | No options required | Try |
split-videoSplit video | segment_duration: 1–3600 seconds · max 50 outputs | Try |
Receive signed terminal job events
Attach a webhook to any job or prebuilt tool request. KinoPipe sends a signed POST after the job reaches succeeded or failed.
Add a webhook to the request
{
"inputs": [{ "id": "main", "url": "https://example.com/source.mp4" }],
"options": { "start": 0, "end": 12 },
"webhook": {
"url": "https://api.example.com/webhooks/kinopipe",
"secret": "replace-with-at-least-16-characters",
"events": ["job.succeeded", "job.failed"]
}
}Payload
{
"id": "6f0462da-…",
"event": "job.succeeded",
"createdAt": "2026-08-24T14:42:18.000Z",
"data": {
"job": {
"id": "f7541d5e-…",
"status": "succeeded",
"consumedCredits": 3
},
"result": {
"output": { "downloadUrl": "https://cdn.kinopipe.com/…" },
"runtimeMs": 2501
}
}
}Verify the signature
Read the raw request body. Compute HMAC-SHA256 over timestamp.rawBody and compare it with the v1 value.
import { createHmac, timingSafeEqual } from "node:crypto"
export function verifyKinoPipeWebhook(rawBody, signatureHeader, secret) {
const fields = Object.fromEntries(
signatureHeader.split(",").map((part) => part.split("=")),
)
const expected = createHmac("sha256", secret)
.update(`${fields.t}.${rawBody}`)
.digest("hex")
const received = Buffer.from(fields.v1 ?? "", "hex")
const computed = Buffer.from(expected, "hex")
return received.length === computed.length
&& timingSafeEqual(received, computed)
}| Header | Value | |
|---|---|---|
| X-KinoPipe-Event | job.succeeded | The event type. |
| X-KinoPipe-Delivery | UUID | Use it to deduplicate deliveries. |
| X-KinoPipe-Signature | t=…,v1=… | Timestamp and HMAC signature. |
KinoPipe rejects credentials in URLs, non-standard ports, local hostnames and private network addresses. A failed callback never changes a successful media job into a failed one.
Pay for measured work
One credit covers one worker second or 6 MiB of output. Successful jobs use whichever rounded-up total is higher, with a one-credit minimum.
KinoPipe checks the balance before processing and returns HTTP 402 with insufficient_credits. There are no automatic overages.
Stable HTTP status and error codes
Every error response includes an error code. Validation errors may also include a structured issues array.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_recipe · invalid_tool_request · invalid_webhook | The request contract or webhook target is invalid. |
| 401 | unauthorized | The API key is missing, invalid or revoked. |
| 402 | insufficient_credits | No processing credits remain. |
| 404 | tool_not_available | The requested preset is not executable. |
| 503 | queue_unavailable | The job could not be placed on the render queue. |
{
"error": "invalid_webhook",
"message": "Webhook URL must resolve only to public addresses"
}Machine-readable API contract
Use the OpenAPI 3.1 document to generate clients, agent tools or validation schemas from the same public contract.