---
title: "Write a fake text story with GPT-Video | GPT-Video API"
description: "Starts writing a new story for a fake text project from your brief, and returns the job to follow."
url: "https://www.gpt-video.com/developers/api/generate-fake-text-story"
operation: "generate_fake_text_story"
method: "POST"
path: "/v1/projects/{projectId}/story/generate"
scope: "spend"
---

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

# Write a fake text story with GPT-Video

Operation id `generate_fake_text_story`. Request `POST /v1/projects/{projectId}/story/generate`.

Starts writing a new story for a fake text project from your brief, 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

Use it to let GPT-Video write the story from a short brief; an empty brief writes a sample story. The answer is 202 with the job: follow it with get_job (`GET /v1/jobs/{jobId}`), then read the story with get_fake_text_story (`GET /v1/projects/{projectId}/story`). The new story replaces the story of the project, and the video follows it; the old story is no longer shown. If a story is already being written for the project, you get that job back and nothing is used (reused is true). Make the voices afterwards with create_fake_text_voices (`POST /v1/projects/{projectId}/story/voices`).

## Example use

A creator who posts a new chat story every morning sends a one-line brief, follows the job with get_job (`GET /v1/jobs/{jobId}`), and reads the result with get_fake_text_story (`GET /v1/projects/{projectId}/story`) before making the voices.

## Endpoint

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

- 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). |

### Body

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `brief` | string | Required | What the story is about, up to 4000 characters. An empty brief writes a sample story. At most 4000 characters. |

Send only the fields listed here. Any other field is refused with UNKNOWN_PARAMETER.

## Request

Write a story from a brief.

```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_ba8b07a88bfb14585459/story/generate" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "brief": "Two roommates argue about who ate the last slice of pizza"
  }'
```

```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_ba8b07a88bfb14585459/story/generate", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "brief": "Two roommates argue about who ate the last slice of pizza"
  }),
});
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_ba8b07a88bfb14585459/story/generate",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
    json={
        "brief": "Two roommates argue about who ate the last slice of pizza"
    },
)
data = r.json()
print(data)
```

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "generate_fake_text_story",
    "arguments": {
      "projectId": "prj_ba8b07a88bfb14585459",
      "brief": "Two roommates argue about who ate the last slice of pizza",
      "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. |
| `reused` | boolean | true when a story was already running for this project and this is its job, paid by 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

- `Location: https://api.gpt-video.com/v1/jobs/job_7d2c9a4e1f0b4386c5a2`

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

**All response fields**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `job` | Job object | Required | Described in [Job object](/developers/objects.md#job). |
| `reused` | boolean | Required | true when a story was already running for this project and this is its job, paid by 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**

#### A story that is already being written

`POST /v1/projects/prj_ba8b07a88bfb14585459/story/generate`

```json title="Request body"
{
  "brief": "A text from an old friend"
}
```

```json title="HTTP 202"
{
  "job": {
    "status": "running",
    "progress": 0.4,
    "errorMessage": null,
    "projectId": "prj_ba8b07a88bfb14585459",
    "compositionId": null,
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null,
    "createdAt": "2026-08-31T22:10:00.000Z",
    "startedAt": "2026-08-31T22:11:00.000Z",
    "finishedAt": null,
    "id": "job_a84eefa1e7fd196089dd",
    "kind": "story"
  },
  "reused": true,
  "paidWith": null
}
```

#### The fake text scripts of the day are used up

`POST /v1/projects/prj_ba8b07a88bfb14585459/story/generate`

```json title="Request body"
{
  "brief": "A wrong number"
}
```

```json title="HTTP 429"
{
  "error": {
    "code": "DAILY_LIMIT_REACHED",
    "message": "Daily limit of fake text scripts reached (100). It resets at 00:00 UTC. Upgrade your plan for a higher limit.",
    "requestId": "req_3c8e0a5d7b2f49146e9c1a8d"
  }
}
```

#### A project that is not a fake text project

`POST /v1/projects/prj_7c2e9a4f1b3d4e8096a5/story/generate`

```json title="Request body"
{
  "brief": "A wrong number"
}
```

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "This project is not a fake text project. Stories exist only in fake text projects.",
    "requestId": "req_7f1d4b8e2a6c40935b7e2d9a"
  }
}
```

#### Writing stories through the API is off

`POST /v1/projects/prj_ba8b07a88bfb14585459/story/generate`

```json title="Request body"
{
  "brief": "A wrong number"
}
```

```json title="HTTP 503"
{
  "error": {
    "code": "UNAVAILABLE",
    "message": "Writing a story through the API is not available yet.",
    "requestId": "req_5d0a8c3e6f1b42977c4a0e5b"
  }
}
```

## Errors

| Code | Status | When | What to do |
| --- | --- | --- | --- |
| [`INSUFFICIENT_CREDITS`](/developers/errors.md#INSUFFICIENT_CREDITS) | 402 | See the error reference. | 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 | See the error reference. | Change the request, then send it again. |
| [`CONFLICT`](/developers/errors.md#CONFLICT) | 409 | A project that is not a fake text project | Read the resource again, then decide. Do not send the same request again. |
| [`DAILY_LIMIT_REACHED`](/developers/errors.md#DAILY_LIMIT_REACHED) | 429 | The fake text scripts of the day are used up | 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

Uses one fake text script of the plan's daily allowance, which resets at 00:00 UTC. When the API bonus pays, it also uses one bonus credit. Nothing is returned if the work fails.

- Paid by the API bonus, it uses 1 bonus credit.
- When the API bonus pays, an account can make 10 fake text scripts a day.
- See [API pricing](/developers/pricing.md) for who pays first.

## Related operations

- [`get_job`](/developers/api/get-job.md) (`GET /v1/jobs/{jobId}`) Get a job
- [`get_fake_text_story`](/developers/api/get-fake-text-story.md) (`GET /v1/projects/{projectId}/story`) Get the story of a fake text project
- [`update_fake_text_story`](/developers/api/update-fake-text-story.md) (`PUT /v1/projects/{projectId}/story`) Write the story of a fake text project
- [`create_fake_text_voices`](/developers/api/create-fake-text-voices.md) (`POST /v1/projects/{projectId}/story/voices`) Make the voices of a fake text story
- [`create_fake_text_project`](/developers/api/create-fake-text-project.md) (`POST /v1/projects/fake-text`) Create a fake text project

## Questions

### What happens to the story the project had?

The new story replaces it: the project and its video show only the new one. Read it with get_fake_text_story (`GET /v1/projects/{projectId}/story`) once the job is done.

### Is the script returned if writing the story fails?

No. The script of the day is used when the request is accepted. Ask for a new story again; a retry with the same Idempotency-Key never uses a second one.
