Clips AI jobs
Autoflip, reframe, and crop as async Partner jobs - starting them, tracking them, and reading the result.
Reframing a clip to a different aspect ratio is an async job, not an
interactive editing session. You start it, you get a job id, and the result
arrives later as a webhook.
Endpoints
| Method | Path | Body | Produces |
|---|---|---|---|
POST | /clips/{id}/autoflip | { "aspect_ratio": "9:16" } | A tracked reframe that follows the action |
POST | /clips/{id}/reframe | { "aspect_ratio": "9:16" } | A tracked reframe |
POST | /clips/{id}/crop | { "aspect_ratio": "9:16", "crop_rect": { "x_px": 0, "y_px": 0, "width_px": 608, "height_px": 1080 } } | A fixed crop window, no tracking |
All three require clips:write and return 202. None of them modify the
original clip - each adds a new entry to the clip's variants.
crop takes a fixed rectangle in source pixels, so compute it against the
clip's own resolution. Because the rectangle is explicit, you can render a
preview in your own UI before submitting the job.
Starting a job
Always send an Idempotency-Key. A retry after a network timeout then cannot
start a second render of the same clip - you get the original job id back.
curl -X POST "https://studio.zentag.ai/api/v1/partner/clips/clip_123/autoflip" \
-H "Authorization: Bearer $ZENTAG_SECRET_KEY" \
-H "Idempotency-Key: autoflip-clip_123-9x16" \
-H "Content-Type: application/json" \
-d '{"aspect_ratio":"9:16"}'{
"object": "job",
"id": "job_autoflip_9f2c1d4e7a8b3c50",
"type": "clip.autoflip",
"status": "queued",
"clip_id": "clip_123",
"aspect_ratio": "9:16",
"created_at": "2026-07-01T18:20:00.000Z"
}Store id. It is how you tie the eventual webhook back to this request.
Getting the result
Webhooks are the primary signal. When the render finishes, Zentag delivers
clip.ready with job_id set to the id above:
{
"id": "evt_1a2b3c",
"type": "clip.ready",
"job_id": "job_autoflip_9f2c1d4e7a8b3c50",
"idempotency_key": "autoflip-clip_123-9x16",
"data": {
"object": "clip",
"id": "clip_123",
"variants": [
{ "aspect_ratio": "9:16", "video_url": "https://...", "status": "completed", "event": "autoFlip" }
]
}
}The output is in data.variants, not in video_url - that remains the original
rendition. Match on job_id: a clip that has been autoflipped and then cropped
emits two clip.ready events, and job_id is what distinguishes them.
On failure you get clip.failed with the same job_id.
Reconciling without webhooks
If a delivery was missed, or you are backfilling, read the job state directly:
| Endpoint | Returns |
|---|---|
GET /jobs/{id} | One job by its id |
GET /clips/{id}/jobs | Every job started for a clip, newest first |
{
"object": "job",
"id": "job_autoflip_9f2c1d4e7a8b3c50",
"type": "clip.autoflip",
"status": "completed",
"clip_id": "clip_123",
"aspect_ratio": "9:16",
"error_message": null,
"idempotency_key": "autoflip-clip_123-9x16",
"created_at": "2026-07-01T18:20:00.000Z",
"completed_at": "2026-07-01T18:22:41.000Z"
}When status is completed, re-fetch GET /clips/{id} to pick up the new
variant. When it is failed, error_message explains why.
Use this to recover, not as your main loop. Polling in a tight loop is the
most common way integrations exhaust their rate limit - subscribe to the
webhooks and poll only when you have reason to think you missed one.
Job statuses
| Status | Meaning |
|---|---|
queued | Accepted, not started |
processing | Rendering |
completed | Done. The result is in the clip's variants |
failed | Terminal. See error_message |
Updated about 2 months ago
