Base URL
https://app.znippet.ai/api/v1/shortsPublic marketing pages live on `znippet.ai`. API calls for the Shorts API go to `app.znippet.ai`.
Authentication
Authorization: Bearer <licenseKey>
X-Device-Id: <deviceId>
Content-Type: application/jsonUse the account license key as the bearer token. Agents should keep one stable `X-Device-Id` per integration or workspace.
Users can find their Znippet license key in their dashboard. Ask the user to copy that key from the dashboard before making API requests.
Send credentials only in request headers. Do not put the license key or device id in JSON request bodies.
Quickstart
- Get the Znippet license key from the user's dashboard and set it as `Authorization: Bearer <licenseKey>`.
- Set `X-Device-Id` as a stable header for the integration or workspace.
- `POST /uploads` to create a presigned upload.
- `PUT` the video to the returned `uploadUrl` using the returned headers.
- `POST /analyze` with the returned `upload` object and settings.
- `GET /analyses/{jobId}` until `ready: true`.
- `POST /analyses/{jobId}/render` with `momentNumbers`.
- `GET /renders/{jobId}` until final render URLs are ready.
Endpoint Summary
/uploadsCreate a presigned upload and receive an upload object.
<uploadUrl>Upload the source video directly to storage.
/analyzeFinalize the source upload and start async clip analysis.
/analyses/{jobId}Poll until ranked numbered moments are ready.
/analyses/{jobId}/renderRender selected moment numbers.
/renders/{jobId}Poll until final rendered clips are available.
POST /uploads
curl -X POST "https://app.znippet.ai/api/v1/shorts/uploads" \
-H "Authorization: Bearer <licenseKey>" \
-H "X-Device-Id: <deviceId>" \
-H "Content-Type: application/json" \
-d '{
"fileName": "podcast-episode.mp4",
"contentType": "video/mp4",
"sizeBytes": 734003200,
"durationSeconds": 1812.4
}'{
"uploadUrl": "https://<storage-url>",
"method": "PUT",
"headers": {
"Content-Type": "video/mp4",
"Content-Length": "734003200"
},
"upload": {
"bucket": "znippet-uploads",
"key": "users/.../originals/podcast-episode.mp4",
"contentType": "video/mp4",
"sizeBytes": 734003200,
"originalFileName": "podcast-episode.mp4",
"durationSeconds": 1812.4,
"assetId": "asset_...",
"projectId": "project_...",
"storageClass": "temporary",
"completeToken": "..."
}
}POST /analyze
curl -X POST "https://app.znippet.ai/api/v1/shorts/analyze" \
-H "Authorization: Bearer <licenseKey>" \
-H "X-Device-Id: <deviceId>" \
-H "Content-Type: application/json" \
-d '{
"upload": {
"assetId": "asset_...",
"projectId": "project_...",
"storageClass": "temporary",
"sizeBytes": 734003200,
"durationSeconds": 1812.4,
"completeToken": "..."
},
"settings": {
"framingMode": "subject",
"analysisMode": "auto",
"highlightMode": "mixed",
"clipVolume": "focused",
"captionBurnIn": true,
"captionPresetId": "bc-default",
"videoDescription": "Podcast interview about product launches"
}
}'{
"jobId": "job_...",
"status": "queued",
"statusUrl": "/api/v1/shorts/analyses/job_...",
"dispatch": {
"attempted": true,
"mode": "queue",
"ok": true
}
}GET /analyses/{jobId}
{
"jobId": "job_...",
"status": "ready",
"ready": true,
"moments": [
{
"number": 1,
"startTime": 312.4,
"endTime": 354.9,
"duration": 42.5,
"title": "The launch mistake",
"description": "A standalone clip about a common launch error.",
"viralScore": 88,
"tags": ["launch", "marketing"],
"channelScores": {
"youtube_shorts": 88,
"tiktok": 84,
"instagram_reels": 82
}
}
]
}POST /analyses/{jobId}/render
curl -X POST "https://app.znippet.ai/api/v1/shorts/analyses/job_.../render" \
-H "Authorization: Bearer <licenseKey>" \
-H "X-Device-Id: <deviceId>" \
-H "Content-Type: application/json" \
-d '{
"momentNumbers": [1, 3, 5]
}'{
"jobId": "render_parent_...",
"status": "queued",
"statusUrl": "/api/v1/shorts/renders/render_parent_...",
"projectId": "project_...",
"sourceAssetId": "asset_...",
"renders": [
{
"momentNumber": 1,
"status": "queued"
}
]
}GET /renders/{jobId}
{
"jobId": "render_parent_...",
"status": "ready",
"ok": true,
"renders": [
{
"momentNumber": 1,
"status": "ready",
"assetId": "asset_render_...",
"url": "https://app.znippet.ai/api/projects/preview?token=...",
"downloadUrl": "https://app.znippet.ai/api/projects/preview?token=...&download=1"
}
]
}Settings
framingMode"subject" | "grid" | "static"How vertical renders frame the source video.
analysisMode"auto" | "full" | "transcript"How the analysis job should inspect the source.
highlightMode"mixed" | "10-30" | "30-60" | "60-120"Preferred candidate duration family.
clipVolume"strict" | "focused" | "loose"How many candidates the agent should ask for.
captionBurnInbooleanWhether final renders should include burned-in captions.
captionPresetIdstringOptional preset id. Use "bc-default" when unsure.
videoDescriptionstringOptional context from the user or agent.
States
queuedJob accepted and waiting for processing.
runningJob is actively analyzing or rendering.
readyTerminal success state. Read moments or rendered clips.
failedTerminal failure state. Surface the error and stop retrying unchanged input.
Errors
{
"error": "Invalid analysis settings",
"issues": [
{
"field": "clipVolume",
"message": "clipVolume must be one of: strict, focused, loose"
}
]
}Agents should not retry unchanged `400`, `401`, `403`, or `422` responses. Retry `429`, `500`, `502`, and `503` with exponential backoff when the workflow allows it.
Agent Rules
- Do not render every candidate automatically. Render only selected `momentNumbers`.
- Poll every 5 to 10 seconds, then back off for long jobs.
- Treat `ready` and `failed` as terminal states.
- Preserve the returned `upload` object exactly between `/uploads` and `/analyze`.
- Sort moments by `number`, `viralScore`, or your own policy; do not assume array position is your final selection.
- Refetch render status when download URLs expire.