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 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 to keep going after the bonus. The API pricing page explains each case.
Read the balance
get_credits (GET /v1/credits). Check the balance with the API bonus.
curl "https://api.gpt-video.com/v1/credits" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY"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);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){
"method": "tools/call",
"params": {
"name": "get_credits",
"arguments": {}
}
}Example response
GET /v1/credits
{
"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.statusisactive,used_up,expired,pausedorcancelled. Only anactivebonus can be spent.bonus.remainingis what is left andbonus.expiresAtis when it ends.bonusis null when the account never received a bonus.credits.remainingis 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.planRequiredis true when the account must choose a plan before it can spend its credits.dailyLimitsshows 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 (GET /v1/projects). List the projects.
# 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')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;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
{
"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 (POST /v1/projects/{projectId}/analyze). Not enough credits.
# 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"// 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);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
{
"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 (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 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 (
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, 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 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 lists every code.