---
title: "Analyze a project | GPT-Video API reference"
description: "Starts the analysis that transcribes the source video and finds its clips, and returns the job to follow."
url: "https://www.gpt-video.com/developers/api/analyze-project"
operation: "analyze_project"
method: "POST"
path: "/v1/projects/{projectId}/analyze"
scope: "spend"
---

Do not assume endpoints have parameters other than the ones explicitly specified.

# Analyze a project

Operation id `analyze_project`. Request `POST /v1/projects/{projectId}/analyze`.

Starts the analysis that transcribes the source video and finds its clips, and returns the job to follow.

Beta. This endpoint is supported and can gain new fields. If a change would break your code, we tell you before it takes effect and list it in the changelog.

## About

Call it when missingSettings is empty; estimate_analysis (`POST /v1/projects/{projectId}/analyze/estimate`) gives the price first. The answer is 202 with the job of the analysis: follow it with get_job (`GET /v1/jobs/{jobId}`), and when it is done list the clips with list_clips (`GET /v1/projects/{projectId}/clips`). If an analysis of the project is already running, you get that job back and nothing is charged (reused is true). Retrying with the same Idempotency-Key never charges twice: if the first attempt was charged, the retry answers what it paid and who paid it (paidWith). Analyzing a project again replaces its proposed and rejected clips and keeps the accepted ones.

## Example use

A script that clips every new episode of a show checks the price with estimate_analysis (`POST /v1/projects/{projectId}/analyze/estimate`), then calls analyze_project (`POST /v1/projects/{projectId}/analyze`). It follows the job with get_job (`GET /v1/jobs/{jobId}`) and reads the clips with list_clips (`GET /v1/projects/{projectId}/clips`) when the job is done.

## Endpoint

```text
POST https://api.gpt-video.com/v1/projects/{projectId}/analyze
```

- Scope `spend`.
- Rate limit 10 requests a minute for each key and 20 for each account, shared by every `spend` call. See [rate limits](/developers/rate-limits.md).
- `Idempotency-Key` is required.
- It starts work that runs on its own. The answer comes back before the work is done.

## Parameters

### Path

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `projectId` | string | Required | The project id. It starts with prj_. Matches `^prj_[0-9a-f]{20}$`. |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | Required | This operation spends, so the key is required. Create it once, before the request, and send the same key on every retry. From 1 to 255 visible characters, such as a UUID. See [idempotency](/developers/pagination-and-idempotency.md). |

## Request

Analyze a project.

```bash title="curl"
# Create the key once. Send this same key again if you retry this request.
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST "https://api.gpt-video.com/v1/projects/prj_7c2e9a4f1b3d4e8096a5/analyze" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY"
```

```ts title="TypeScript"
// Create the key once. Send this same key again if you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.gpt-video.com/v1/projects/prj_7c2e9a4f1b3d4e8096a5/analyze", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
  },
});
const data = await res.json();
console.log(data);
```

```python title="Python"
import os
import uuid

import requests

# Create the key once. Send this same key again if you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.post(
    "https://api.gpt-video.com/v1/projects/prj_7c2e9a4f1b3d4e8096a5/analyze",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
)
data = r.json()
print(data)
```

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "analyze_project",
    "arguments": {
      "projectId": "prj_7c2e9a4f1b3d4e8096a5",
      "confirm": true,
      "idempotencyKey": "REPLACE-WITH-A-NEW-UUID-FOR-THIS-CALL"
    }
  }
}
```

## Response

A successful call answers HTTP 202. Returns a [Job object](/developers/objects.md#job) in `job`.

### Fields to check

| Name | Type | Description |
| --- | --- | --- |
| `job.id` | string | The job id. It starts with job_. |
| `job.status` | string | queued and running are still working; done, failed and cancelled are final. |
| `creditsCharged` | integer | The credits this request was charged: the price for a new analysis, 0 when an analysis already running was returned. |
| `reused` | boolean | true when an analysis of the project was already running and this is its job, charged to nobody again. |
| `paidWith` | string or null | Who paid for this request: plan is the credits or the daily allowance of the account's plan, api_bonus is the API bonus. null when this request paid nothing, because the same work was already running. A retry with the same Idempotency-Key says who paid the first time. |

### Response example

```json title="HTTP 202"
{
  "job": {
    "id": "job_b582bc8315e372bf9ba3",
    "kind": "analysis",
    "status": "queued",
    "progress": 0,
    "errorMessage": null,
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "compositionId": null,
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null,
    "createdAt": "2026-09-01T11:10:00.000Z",
    "startedAt": null,
    "finishedAt": null
  },
  "creditsCharged": 30,
  "reused": false,
  "paidWith": "plan"
}
```

**All response fields**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `job` | Job object | Required | Described in [Job object](/developers/objects.md#job). |
| `creditsCharged` | integer | Required | The credits this request was charged: the price for a new analysis, 0 when an analysis already running was returned. At least 0. |
| `reused` | boolean | Required | true when an analysis of the project was already running and this is its job, charged to nobody again. |
| `paidWith` | string or null | Required | Who paid for this request: plan is the credits or the daily allowance of the account's plan, api_bonus is the API bonus. null when this request paid nothing, because the same work was already running. A retry with the same Idempotency-Key says who paid the first time. One of `plan`, `api_bonus`. |

**More examples**

#### An analysis that is already running

`POST /v1/projects/prj_7c2e9a4f1b3d4e8096a5/analyze`

```json title="HTTP 202"
{
  "job": {
    "id": "job_6c1e8b4a2f0d4a97e3b5",
    "kind": "analysis",
    "status": "running",
    "progress": 0.4,
    "errorMessage": null,
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "compositionId": null,
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null,
    "createdAt": "2026-09-01T09:11:00.000Z",
    "startedAt": "2026-09-01T09:12:00.000Z",
    "finishedAt": null
  },
  "creditsCharged": 0,
  "reused": true,
  "paidWith": null
}
```

#### A project without a source video

`POST /v1/projects/prj_1d8b6e0c47a24f93b1c2/analyze`

```json title="HTTP 422"
{
  "error": {
    "code": "SOURCE_MISSING",
    "message": "Upload or import a source video before starting the analysis.",
    "hint": "A project's video cannot be changed through the API. Upload the video, then create a new project with its asset id as sourceAssetId.",
    "requestId": "req_6c77b252f5f65fabd4aa7561"
  }
}
```

#### Settings still to choose

`POST /v1/projects/prj_7c2e9a4f1b3d4e8096a5/analyze`

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "Set these settings before analyzing: captionStyle, reframe. Change them with update_project.",
    "requestId": "req_205c2427bb67aad78f846bdd"
  }
}
```

#### A minimum longer than the maximum

`POST /v1/projects/prj_7c2e9a4f1b3d4e8096a5/analyze`

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "minDurationSec (60) is longer than maxDurationSec (30). Change them with update_project.",
    "requestId": "req_9d4a0c7e2b1f4863a5c0e7d2"
  }
}
```

#### Not enough credits

`POST /v1/projects/prj_7c2e9a4f1b3d4e8096a5/analyze`

```json title="HTTP 402"
{
  "error": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "Out of credits: 10 of 800 left this month and this video needs 30. Top up or upgrade your plan.",
    "hint": "Buy credits or change plan at https://www.gpt-video.com/app/billing.",
    "requestId": "req_3e8b5f1a0c7d4e29b6a4f0c1"
  }
}
```

#### A project that does not exist

`POST /v1/projects/prj_9f0d3b7e1c5a4d28b6f3/analyze`

```json title="HTTP 404"
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Project prj_9f0d3b7e1c5a4d28b6f3 not found",
    "hint": "Check the id. An id that belongs to another account is not found either.",
    "requestId": "req_7a2f9c4e1d6b4058e3b1a9c2"
  }
}
```

## Errors

| Code | Status | When | What to do |
| --- | --- | --- | --- |
| [`INSUFFICIENT_CREDITS`](/developers/errors.md#INSUFFICIENT_CREDITS) | 402 | Not enough credits | Stop and tell the user. |
| [`PLAN_REQUIRED`](/developers/errors.md#PLAN_REQUIRED) | 402 | See the error reference. | Stop and tell the user. |
| [`NOT_FOUND`](/developers/errors.md#NOT_FOUND) | 404 | A project that does not exist | Change the request, then send it again. |
| [`CONFLICT`](/developers/errors.md#CONFLICT) | 409 | Settings still to choose | Read the resource again, then decide. Do not send the same request again. |
| [`SOURCE_MISSING`](/developers/errors.md#SOURCE_MISSING) | 422 | A project without a source video | Change the request, then send it again. |
| [`DAILY_LIMIT_REACHED`](/developers/errors.md#DAILY_LIMIT_REACHED) | 429 | See the error reference. | Stop until the limit resets at 00:00 UTC. |

Every call can also answer these errors. [`VALIDATION_FAILED`](/developers/errors.md#VALIDATION_FAILED), [`UNKNOWN_PARAMETER`](/developers/errors.md#UNKNOWN_PARAMETER), [`IDEMPOTENCY_KEY_REQUIRED`](/developers/errors.md#IDEMPOTENCY_KEY_REQUIRED), [`UNAUTHORIZED`](/developers/errors.md#UNAUTHORIZED), [`SCOPE_MISSING`](/developers/errors.md#SCOPE_MISSING), [`IDEMPOTENCY_IN_PROGRESS`](/developers/errors.md#IDEMPOTENCY_IN_PROGRESS), [`PAYLOAD_TOO_LARGE`](/developers/errors.md#PAYLOAD_TOO_LARGE), [`IDEMPOTENCY_KEY_REUSED`](/developers/errors.md#IDEMPOTENCY_KEY_REUSED), [`RATE_LIMITED`](/developers/errors.md#RATE_LIMITED), [`INTERNAL`](/developers/errors.md#INTERNAL), [`UNAVAILABLE`](/developers/errors.md#UNAVAILABLE).

## Cost

One credit per started minute of source video, charged when the analysis starts and not returned if it fails. Analyzing again is a new analysis and is charged again.

- Paid by the API bonus, it uses the same number of bonus credits.
- When the API bonus pays, 1 of its jobs runs at a time. The next one waits with status `queued`.
- See [API pricing](/developers/pricing.md) for who pays first.

## Related operations

- [`estimate_analysis`](/developers/api/estimate-analysis.md) (`POST /v1/projects/{projectId}/analyze/estimate`) Estimate the credits of an analysis or a build
- [`update_project`](/developers/api/update-project.md) (`PATCH /v1/projects/{projectId}`) Update a project
- [`get_project`](/developers/api/get-project.md) (`GET /v1/projects/{projectId}`) Get a project
- [`get_job`](/developers/api/get-job.md) (`GET /v1/jobs/{jobId}`) Get a job
- [`list_clips`](/developers/api/list-clips.md) (`GET /v1/projects/{projectId}/clips`) List the clips of a project
- [`get_transcript`](/developers/api/get-transcript.md) (`GET /v1/assets/{assetId}/transcript`) Get the transcript of a video

## Questions

### What does an analysis cost?

One credit per started minute of the source video: a 30 minute video costs 30 credits. estimate_analysis (`POST /v1/projects/{projectId}/analyze/estimate`) tells you the price and your balance before you start.

### Are the credits returned if the analysis fails?

No. The credits are charged when the analysis starts, because the work on the video has begun. A retry with the same Idempotency-Key is never charged twice.

### What happens if two requests start the same analysis?

One analysis runs and it is charged once. The other request gets the same job back with creditsCharged 0 and reused true.

### Can I change the settings while the analysis is waiting to start?

Yes. The analysis reads the settings when it starts, so a change made before then is used. The price depends only on the source video, which cannot change.
