---
title: "Handle running out of credits in the API"
description: "What the GPT-Video API answers when the credits or the API bonus run out, and how your code should stop, tell the user or wait for the reset."
url: "https://www.gpt-video.com/developers/guides/out-of-credits"
---

# Handle running out of credits

A call that spends credits can be refused when the account cannot pay. The answer carries a code that says why, and each code needs a different next step. This guide shows how to read the balance, what each refusal means and what your program should do.

The examples read your key from the `GPT_VIDEO_API_KEY` variable, as [Authentication](/developers/authentication.md) explains. The steps use variables, so an id from one answer feeds the next call.

## Who can spend through the API

API bonus: 50 credits. No card needed. API bonus credits can be spent only through the API. After the bonus, the API needs a Creator or Studio plan.

On the Creator and Studio plans, a call that costs credits spends the bonus first when the bonus covers the whole price. Otherwise the plan credits and the top-up pay. On any other plan, a call spends only the bonus. A price the bonus cannot cover in full is refused with `PLAN_REQUIRED`, and the bonus never pays part of a price. [Choose a plan](/pricing) to keep going after the bonus. The [API pricing](/developers/pricing.md) page explains each case.

## Read the balance

[`get_credits`](/developers/api/get-credits.md) (`GET /v1/credits`). Check the balance with the API bonus.

```bash title="curl"
curl "https://api.gpt-video.com/v1/credits" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY"
```

```ts title="TypeScript"
const res = await fetch("https://api.gpt-video.com/v1/credits", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
  },
});
const data = await res.json();
console.log(data);
```

```python title="Python"
import os

import requests

r = requests.get(
    "https://api.gpt-video.com/v1/credits",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
)
data = r.json()
print(data)
```

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "get_credits",
    "arguments": {}
  }
}
```

**Example response**

`GET /v1/credits`

```json title="HTTP 200"
{
  "plan": "free",
  "planRequired": true,
  "credits": {
    "period": "2026-09",
    "periodEndsAt": "2026-10-01T00:00:00.000Z",
    "included": 0,
    "remaining": 0,
    "remainingFromPlan": 0,
    "topUp": 0
  },
  "bonus": {
    "status": "active",
    "granted": 50,
    "remaining": 47,
    "grantedAt": "2026-09-01T08:00:00.000Z",
    "expiresAt": "2026-10-01T08:00:00.000Z",
    "resumesAt": null
  },
  "dailyLimits": {
    "resetsAt": "2026-09-02T00:00:00.000Z",
    "exports": {
      "limit": 5,
      "used": 1,
      "remaining": 4
    },
    "assistantTurns": {
      "limit": 20,
      "used": 1,
      "remaining": 19
    },
    "toolRuns": {
      "limit": 10,
      "used": 0,
      "remaining": 10
    },
    "fakeTextScripts": {
      "limit": 10,
      "used": 0,
      "remaining": 10
    },
    "assistantTurnsThroughApi": null
  }
}
```

Read these fields before you start work that costs credits.

- `bonus.status` is `active`, `used_up`, `expired`, `paused` or `cancelled`. Only an `active` bonus can be spent. `bonus.remaining` is what is left and `bonus.expiresAt` is when it ends.
- `bonus` is null when the account never received a bonus.
- `credits.remaining` is the plan credits and the top-up. It does not include the bonus. It can be spent through the API only on the Creator and Studio plans.
- `planRequired` is true when the account must choose a plan before it can spend its credits.
- `dailyLimits` shows what is left today of exports, Editor Assistant turns, tool runs and fake text scripts.

We can pause the API bonus. While it is paused, a call it would pay for answers `PLAN_REQUIRED`. A bonus that reached its limit for today also shows as paused. Its `bonus.resumesAt` is the next 00:00 UTC, and a call it would pay for answers `DAILY_LIMIT_REACHED`.

## Find a project

[`list_projects`](/developers/api/list-projects.md) (`GET /v1/projects`). List the projects.

```bash title="curl"
# This command needs jq to read the answer.
PROJECT_ID=$(curl -sS "https://api.gpt-video.com/v1/projects" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" | jq -r '.data[0].id')
```

```ts title="TypeScript"
const res = await fetch("https://api.gpt-video.com/v1/projects", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
  },
});
const data = await res.json();
const projectId = data.data[0].id;
```

```python title="Python"
import os

import requests

r = requests.get(
    "https://api.gpt-video.com/v1/projects",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
)
data = r.json()
project_id = data["data"][0]["id"]
```

**Example response**

`GET /v1/projects`

```json title="HTTP 200"
{
  "data": [
    {
      "id": "prj_1d8b6e0c47a24f93b1c2",
      "kind": "autoclip",
      "title": "Weekly podcast",
      "status": "draft",
      "sourceAssetId": null,
      "compositionId": null,
      "settings": {
        "language": null,
        "clipCount": 5,
        "minDurationSec": null,
        "maxDurationSec": null,
        "objective": "educational",
        "captionStyle": null,
        "reframe": null,
        "instructions": null,
        "vocabulary": null
      },
      "missingSettings": [
        "language",
        "minDurationSec",
        "maxDurationSec",
        "captionStyle",
        "reframe"
      ],
      "splitScreen": null,
      "streamer": null,
      "redditStory": null,
      "fakeText": null,
      "vibeEdit": null,
      "errorMessage": null,
      "createdAt": "2026-09-01T09:50:00.000Z",
      "updatedAt": "2026-09-01T09:50:00.000Z"
    },
    {
      "id": "prj_7c2e9a4f1b3d4e8096a5",
      "kind": "autoclip",
      "title": "Founder interview",
      "status": "ready",
      "sourceAssetId": "ast_3f9a1c7e5b2d4086a1c4",
      "compositionId": null,
      "settings": {
        "language": "en",
        "clipCount": 3,
        "minDurationSec": 15,
        "maxDurationSec": 60,
        "objective": "viral",
        "captionStyle": "word-pop",
        "reframe": "face",
        "instructions": null,
        "vocabulary": null
      },
      "missingSettings": [],
      "splitScreen": null,
      "streamer": null,
      "redditStory": null,
      "fakeText": null,
      "vibeEdit": null,
      "errorMessage": null,
      "createdAt": "2026-09-01T09:10:00.000Z",
      "updatedAt": "2026-09-01T09:40:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

The example keeps the newest project in `projectId`, so the next step has something to start.

## When the answer is INSUFFICIENT_CREDITS

[`analyze_project`](/developers/api/analyze-project.md) (`POST /v1/projects/{projectId}/analyze`). Not enough credits.

```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/$PROJECT_ID/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/${projectId}/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(
    f"https://api.gpt-video.com/v1/projects/{project_id}/analyze",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
)
data = r.json()
print(data)
```

**Example response**

`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"
  }
}
```

This call starts a real analysis when the account can pay for it. When it cannot, the answer is status 402 and the code `INSUFFICIENT_CREDITS`. The message says how many credits are left and how many the work needs, and the hint says where to add credits. Nothing was charged.

Do not send the request again by itself. It fails the same way until a person adds credits or changes the plan in the GPT-Video app. Tell your user, and stop the work that needs the credits.

Call [estimate_analysis](/developers/api/estimate-analysis.md) (`POST /v1/projects/{projectId}/analyze/estimate`) before you start work, and you see this answer only when the balance changed in between. [Estimate the cost before you spend credits](/developers/guides/estimate-before-spending.md) shows how.

## When the answer is PLAN_REQUIRED

`PLAN_REQUIRED` has status 402 too. It means the call needs a plan that the account does not have, or that the bonus could not pay. It comes in these cases.

- The account is not on the Creator or Studio plan, and its bonus is used up, expired or cancelled.
- The account is not on the Creator or Studio plan, and we paused the bonus.
- The account is not on the Creator or Studio plan, and the price is higher than what is left in the bonus. The bonus pays a whole price or none of it.
- An export is longer than the API bonus pays for. [create_export](/developers/api/create-export.md) (`POST /v1/compositions/{compositionId}/exports`) gives the answer for this case.

The message and the hint say which case it is. Do not retry. Tell your user to choose a Creator or Studio plan on the [pricing page](/pricing), then send the call again.

## When the answer is DAILY_LIMIT_REACHED

`DAILY_LIMIT_REACHED` has status 429. A daily limit is used up, or the API bonus reached its limit for today. Every daily limit starts again at 00:00 UTC.

Stop sending calls of that kind until then. Retrying earlier fails the same way. Read `dailyLimits` from the balance to see which limit is used up. The API bonus has its own daily limits.

| Limit while the API bonus pays | Value |
| --- | --- |
| Exports a day | 5 |
| Longest export | 600 seconds |
| Editor Assistant turns a day | 20 |
| Tool runs a day | 10 |
| Fake text scripts a day | 10 |
| Jobs running at once | 1 |

[Rate limits](/developers/rate-limits.md) are a different case. `RATE_LIMITED` means you sent requests too fast. Wait the seconds in `Retry-After` and send the same request again.

## What your program should do

| Code | Status | What to do |
| --- | --- | --- |
| `INSUFFICIENT_CREDITS` | 402 | Stop the work and tell your user to add credits in the app. |
| `PLAN_REQUIRED` | 402 | Stop the work and tell your user to choose a Creator or Studio plan. |
| `DAILY_LIMIT_REACHED` | 429 | Stop that kind of call until 00:00 UTC and tell your user when it starts again. |
| `RATE_LIMITED` | 429 | Wait for `Retry-After`, then send the same request. |

None of the first three is worth a timer-based retry. They end when a person acts, or when the day changes.

## After the account can pay

Read the estimate again, then send the call. If you are not sure whether an earlier try was charged, send the call with the same `Idempotency-Key` as before. A retry never charges twice, and it answers what was paid and by whom. [GPT-Video API errors and what to do next](/developers/errors.md) lists every code.
