Skip to content

Webhooks

A webhook is an address of your app that GPT-Video calls when a job or an export ends. Your program learns that the work is over without reading the job again and again. The events follow the Standard Webhooks specification, so any library made for it can check them.

What an event tells you

An event says that something changed. It carries the ids of what changed, and never a file or a download link. Read the current state with get_job (GET /v1/jobs/{jobId}) or get_export (GET /v1/exports/{exportId}) before you act. For a finished export, get_export gives the download link.

The events are listed below, each with its body and an example. A job event is sent for a job you can read with get_job (GET /v1/jobs/{jobId}). An export event is sent for an export you can read with get_export (GET /v1/exports/{exportId}).

Add a webhook

In the GPT-Video app, open the Developers page, at https://www.gpt-video.com/app/developers, and add the address under Webhooks. The address must be reachable from the internet. Addresses on a private network are refused.

The Developers page shows the secret of the webhook. It starts with whsec_. Keep it on your server, for example in an environment variable, and never in code that runs in a browser.

From the same page you can send a test event, webhook.test. It is sent once, and it does not count when a webhook keeps failing.

Check the signature

Every event comes with three headers.

  • webhook-id is the id of the event. It is the same on every attempt to send it.
  • webhook-timestamp is when this attempt was sent, in seconds since the Unix epoch.
  • webhook-signature is v1, followed by the base64 HMAC-SHA256 of the id, the timestamp and the raw body, joined by dots.

The key of the HMAC is the part of the secret after whsec_, decoded from base64. Compute the signature on the raw body, before you parse the JSON. A body that was parsed and written again has different bytes, and a real event would then fail the check.

Refuse an event whose timestamp is more than five minutes away from your clock. While a secret is being rotated, webhook-signature carries two signatures separated by a space. Accept the event when either one matches.

Receive webhooks in your app has a receiver in TypeScript and in Python that does all of this.

Delivery

An event can arrive more than once, and events can arrive in a different order from the one in which things happened. A retry carries the same webhook-id and a new webhook-timestamp. Keep the ids you handled and skip an id you have seen.

Answer with a 2xx status as soon as the signature checks out, and do the slow work after the answer. Since events can arrive out of order, decide from the state that get_job (GET /v1/jobs/{jobId}) or get_export (GET /v1/exports/{exportId}) returns.

We keep the records of webhook deliveries for thirty days, as the privacy notice says.

Webhooks or waiting

A webhook needs an address that the internet can reach. A script or an AI agent without one can wait instead. get_job (GET /v1/jobs/{jobId}) and get_export (GET /v1/exports/{exportId}) take ?wait= to hold the request until the work ends. Wait for long jobs explains how. Both return the same state, so you can use either one, or both.

Events

EventWhen it is sent
job.completedSent when a job you can read with get_job (GET /v1/jobs/{jobId}) finishes with status done. Call get_job to read it.
job.failedSent when a job finishes with status failed. A job that is tried again is not failed yet. Call get_job (GET /v1/jobs/{jobId}) to read why.
job.cancelledSent when a job is cancelled before it finishes.
export.completedSent when an export is done. Call get_export (GET /v1/exports/{exportId}) to download its file.
export.failedSent when an export fails, including an export that never started. Call get_export (GET /v1/exports/{exportId}) to read why.
export.cancelledSent when an export is cancelled before it finishes.
webhook.testSent when you send a test event from the Developers page. It is never sent again and does not count when a webhook keeps failing.

Headers of every event

NameTypeRequiredDescription
webhook-idstringRequiredThe id of the event. It is the same on every attempt to send the event, so use it to skip an event you already handled.
webhook-timestampstringRequiredWhen this attempt was sent, in seconds since the Unix epoch. A retry carries a new time. It is part of what is signed.
webhook-signaturestringRequiredv1, then a comma, then the base64 HMAC-SHA256 of the webhook-id, the webhook-timestamp and the raw body joined by dots, signed with the secret of the webhook (whsec_...). The key is the part of the secret after whsec_, decoded from base64. While a secret is being rotated it carries two signatures separated by a space, and either one is valid. Any Standard Webhooks library can check it.

job.completed

Sent when a job you can read with get_job (GET /v1/jobs/{jobId}) finishes with status done. Call get_job to read it.

Body

NameTypeRequiredDescription
typestringRequiredThe event type. Always job.completed.
timestampstringRequiredWhen the event happened, in UTC.
dataobjectRequired
data.jobIdstringRequiredThe job, to read with get_job (GET /v1/jobs/{jobId}).
data.kindstringRequiredWhat the job does, as get_job (GET /v1/jobs/{jobId}) says it. One of analysis, build, export, framing, import, story, voices, tool, assistant_turn.
data.projectIdstring or nullRequiredThe project the job works on. null when the job has none.
data.compositionIdstring or nullRequiredThe composition the job works on. null when the job has none.
data.exportIdstring or nullRequiredThe export the job renders. null when the job has none.
data.assetIdstring or nullRequiredThe video an import brought in. null when the job has none.
data.toolRunIdstring or nullRequiredThe tool run the job carries out. null when the job has none.
data.turnIdstring or nullRequiredThe Editor Assistant turn the job carries out. null when the job has none.
job.completed example
{
  "type": "job.completed",
  "timestamp": "2026-09-01T09:40:00.000Z",
  "data": {
    "jobId": "job_6c1e8b4a2f0d4a97e3b5",
    "kind": "analysis",
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "compositionId": null,
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null
  }
}

job.failed

Sent when a job finishes with status failed. A job that is tried again is not failed yet. Call get_job (GET /v1/jobs/{jobId}) to read why.

Body

NameTypeRequiredDescription
typestringRequiredThe event type. Always job.failed.
timestampstringRequiredWhen the event happened, in UTC.
dataobjectRequired
data.jobIdstringRequiredThe job, to read with get_job (GET /v1/jobs/{jobId}).
data.kindstringRequiredWhat the job does, as get_job (GET /v1/jobs/{jobId}) says it. One of analysis, build, export, framing, import, story, voices, tool, assistant_turn.
data.projectIdstring or nullRequiredThe project the job works on. null when the job has none.
data.compositionIdstring or nullRequiredThe composition the job works on. null when the job has none.
data.exportIdstring or nullRequiredThe export the job renders. null when the job has none.
data.assetIdstring or nullRequiredThe video an import brought in. null when the job has none.
data.toolRunIdstring or nullRequiredThe tool run the job carries out. null when the job has none.
data.turnIdstring or nullRequiredThe Editor Assistant turn the job carries out. null when the job has none.
job.failed example
{
  "type": "job.failed",
  "timestamp": "2026-09-01T09:40:00.000Z",
  "data": {
    "jobId": "job_6c1e8b4a2f0d4a97e3b5",
    "kind": "analysis",
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "compositionId": null,
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null
  }
}

job.cancelled

Sent when a job is cancelled before it finishes.

Body

NameTypeRequiredDescription
typestringRequiredThe event type. Always job.cancelled.
timestampstringRequiredWhen the event happened, in UTC.
dataobjectRequired
data.jobIdstringRequiredThe job, to read with get_job (GET /v1/jobs/{jobId}).
data.kindstringRequiredWhat the job does, as get_job (GET /v1/jobs/{jobId}) says it. One of analysis, build, export, framing, import, story, voices, tool, assistant_turn.
data.projectIdstring or nullRequiredThe project the job works on. null when the job has none.
data.compositionIdstring or nullRequiredThe composition the job works on. null when the job has none.
data.exportIdstring or nullRequiredThe export the job renders. null when the job has none.
data.assetIdstring or nullRequiredThe video an import brought in. null when the job has none.
data.toolRunIdstring or nullRequiredThe tool run the job carries out. null when the job has none.
data.turnIdstring or nullRequiredThe Editor Assistant turn the job carries out. null when the job has none.
job.cancelled example
{
  "type": "job.cancelled",
  "timestamp": "2026-09-01T09:40:00.000Z",
  "data": {
    "jobId": "job_6c1e8b4a2f0d4a97e3b5",
    "kind": "analysis",
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "compositionId": null,
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null
  }
}

export.completed

Sent when an export is done. Call get_export (GET /v1/exports/{exportId}) to download its file.

Body

NameTypeRequiredDescription
typestringRequiredThe event type. Always export.completed.
timestampstringRequiredWhen the event happened, in UTC.
dataobjectRequired
data.exportIdstringRequiredThe export, to read with get_export (GET /v1/exports/{exportId}).
data.compositionIdstringRequiredThe composition the export renders.
data.projectIdstringRequiredThe project of the composition.
data.jobIdstring or nullRequiredThe job that renders the export. null for an export that never started.
export.completed example
{
  "type": "export.completed",
  "timestamp": "2026-09-01T10:15:00.000Z",
  "data": {
    "exportId": "rnd_0f6b3e9d2c7a4158b4a3",
    "compositionId": "cmp_4e8c2a6f09d14b73a1e5",
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "jobId": "job_a3e9d05c7b1f4e2896d4"
  }
}

export.failed

Sent when an export fails, including an export that never started. Call get_export (GET /v1/exports/{exportId}) to read why.

Body

NameTypeRequiredDescription
typestringRequiredThe event type. Always export.failed.
timestampstringRequiredWhen the event happened, in UTC.
dataobjectRequired
data.exportIdstringRequiredThe export, to read with get_export (GET /v1/exports/{exportId}).
data.compositionIdstringRequiredThe composition the export renders.
data.projectIdstringRequiredThe project of the composition.
data.jobIdstring or nullRequiredThe job that renders the export. null for an export that never started.
export.failed example
{
  "type": "export.failed",
  "timestamp": "2026-09-01T10:15:00.000Z",
  "data": {
    "exportId": "rnd_0f6b3e9d2c7a4158b4a3",
    "compositionId": "cmp_4e8c2a6f09d14b73a1e5",
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "jobId": "job_a3e9d05c7b1f4e2896d4"
  }
}

export.cancelled

Sent when an export is cancelled before it finishes.

Body

NameTypeRequiredDescription
typestringRequiredThe event type. Always export.cancelled.
timestampstringRequiredWhen the event happened, in UTC.
dataobjectRequired
data.exportIdstringRequiredThe export, to read with get_export (GET /v1/exports/{exportId}).
data.compositionIdstringRequiredThe composition the export renders.
data.projectIdstringRequiredThe project of the composition.
data.jobIdstring or nullRequiredThe job that renders the export. null for an export that never started.
export.cancelled example
{
  "type": "export.cancelled",
  "timestamp": "2026-09-01T10:15:00.000Z",
  "data": {
    "exportId": "rnd_0f6b3e9d2c7a4158b4a3",
    "compositionId": "cmp_4e8c2a6f09d14b73a1e5",
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "jobId": "job_a3e9d05c7b1f4e2896d4"
  }
}

webhook.test

Sent when you send a test event from the Developers page. It is never sent again and does not count when a webhook keeps failing.

Body

NameTypeRequiredDescription
typestringRequiredThe event type. Always webhook.test.
timestampstringRequiredWhen the event happened, in UTC.
dataobjectRequired
data.endpointIdstringRequiredThe webhook the test was sent to.
webhook.test example
{
  "type": "webhook.test",
  "timestamp": "2026-10-03T12:00:00.000Z",
  "data": {
    "endpointId": "whk_ea7c528e7e454010d9fc"
  }
}