Skip to content

Event types

Events are grouped by domain. Each type is a stable string — pick the ones your integration cares about when registering the webhook.

Fired by the legacy ingest path when an upload is processed.

TypeWhen
asset.creation.succeededAsset successfully transcoded and ready for analysis
asset.creation.failedTranscode failed (corrupt source, unsupported codec, network error)

This is the event most automations subscribe to. It fires when the analyze → transcribe → suggest → preview pipeline completes for an asset.

TypeWhen
asset.analysis.startedPipeline accepted, work queued
asset.analysis.progressPeriodic progress update (high volume — only subscribe if you display progress)
asset.analysis.succeededClips are ready. GET /v1/clips/:jobId will now return populated clips[] with previews.
asset.analysis.failedPipeline errored. The folder doc has a structured pipelineError; the dashboard surfaces the same message.

Fired by the full-quality render pipeline triggered via POST /v1/clips/:clipId/renders.

TypeWhen
project.render.startedRender queued
project.render.progressPeriodic progress update (high volume)
project.render.succeededRender done. GET /v1/renders/:renderId will return output_url.
project.render.failedRender errored

Fired across the post lifecycle — from the moment AI-generated drafts are created (when a clip’s preview becomes ready) through to the platform publish.

TypeWhen
post.draft.createdA draft post was created for a clip. One event fires per platform — GET /v1/clips/:clipId/posts will now return it.
post.scheduledA draft was scheduled for a future publish time via POST /v1/posts/:id/publish with a scheduled_at.
post.publishedThe post landed on the platform. published_url is included for direct linking; GET /v1/posts/:id has the same data.
post.failedPublish failed permanently. The error field has a human-readable reason.

Every delivery shares the same envelope:

{
"id": "<unique event id, use for dedup>",
"type": "<event type>",
"created": <unix-ms timestamp>,
"data": { ... event-specific, see table below ... }
}

The data shape per event is intentionally small — webhooks are notifications, not full responses. Use the V1 GET endpoints to fetch clips, output URLs, and detailed state. That keeps signed URLs fresh, the HMAC verification simple, and lets your handler ack in milliseconds.

Eventdata shape
asset.creation.succeeded{ asset_id, team_id, duration_seconds, job_id }
asset.creation.failed{ asset_id, team_id, error, job_id }
asset.analysis.started{ asset_id, team_id, job_id }
asset.analysis.progress{ asset_id, team_id, progress, job_id } (0-1)
asset.analysis.succeeded{ asset_id, team_id, job_id }
asset.analysis.failed{ asset_id, team_id, error, job_id }
project.render.started{ project_id, team_id }
project.render.progress{ project_id, team_id, progress } (0-1)
project.render.succeeded{ project_id, team_id }
project.render.failed{ project_id, team_id, error }
post.draft.created{ post_id, clip_id, platform, team_id }
post.scheduled{ post_id, clip_id, platform, team_id, scheduled_at }
post.published{ post_id, clip_id, platform, team_id, published_url }
post.failed{ post_id, clip_id, platform, team_id, error }

job_id is the job_id returned by POST /v1/clips. Receivers should use it directly with GET /v1/clips/:jobId — no asset_id-to-job_id lookup needed. It’s emitted on every asset.* event for clip jobs started via the public API.

{
"id": "9f8e7d6c-...",
"type": "asset.analysis.succeeded",
"created": 1745800000000,
"data": {
"asset_id": "asset_xyz",
"team_id": "team_abc123",
"job_id": "fb9a7c3e-..."
}
}

Then call GET /v1/clips/<job_id> to fetch the generated clips. The job_id is the same value you got back from POST /v1/clips.

{
"id": "fb9a7c3e-...",
"type": "project.render.succeeded",
"created": 1745800045000,
"data": {
"project_id": "9d8e7f6a-...",
"team_id": "team_abc123"
}
}

Then call GET /v1/renders/<project_id> to fetch the signed output_url.

{
"id": "ab12cd34-...",
"type": "asset.analysis.failed",
"created": 1745800000000,
"data": {
"asset_id": "asset_xyz",
"team_id": "team_abc123",
"job_id": "fb9a7c3e-...",
"error": "We couldn't find the uploaded video. Try uploading again."
}
}
{
"id": "1c2b3a4d-...",
"type": "post.published",
"created": 1745800120000,
"data": {
"post_id": "9d8e7f6a-...",
"clip_id": "fc3eb2a1-...",
"platform": "youtube",
"team_id": "team_abc123",
"published_url": "https://www.youtube.com/watch?v=abc123"
}
}

Call GET /v1/posts/<post_id> for the full post record.

We only add new fields, never remove or rename. Parse defensively and ignore unknown keys.

The *.progress events fire every few seconds during long jobs. If you subscribe to them, make sure your endpoint can absorb the burst — a 30-minute analysis can produce ~100 progress events. Prefer *.succeeded / *.failed for state-change-driven flows.