Credits and limits
What each feature charges in credits, the ceilings every plan enforces, and the codes an API call returns when one of them refuses the work.
Two independent things can stop a job: the workspace's credit balance and the
plan's ceilings. Both refuse before any work is queued, and both use the same
warning-* vocabulary whether they arrive on a 400 or on a failed job.
Credits
Credits are the display unit and tokens the accounting unit: 1 credit is 1000 tokens. Every endpoint that reports a balance reports both, so a client never has to know the ratio:
curl https://app.shortfast.com/api/v1/workspace \
-H "Authorization: Bearer sf_live_xxxxxxxxxxxxxxxxxxxxxxxx"{
"data": {
"id": "k9WmT3xR2pQ7nV4bd",
"plan": "pro",
"credits": { "credits": 42500, "tokens": 42500000 },
"limits": { },
"usage": { }
}
}What each feature costs
GET /reference/credit-costs returns this table live, in both units and with the
unit each cost applies to. Read it rather than hardcoding: prices move.
| Feature | Credits | Tokens | Per |
|---|---|---|---|
| Video render | 1.8 | 1800 | minute |
| Subtitles and b-rolls | 1.2 | 1200 | minute |
| AI avatar | 300 | 300000 | minute |
| AI image | 1 | 1000 | image |
| Script or description | 0.08 | 80 | generation |
| Premium voice | 3 | 3000 | minute, on top of the standard voice |
Duration-based costs are charged against the video's own length, so a 40 second
render costs about two thirds of a minute's worth. The premium voice line is what
voiceProvider: "gemini" adds over openai, per minute of narration.
Estimate before you queue: multiply the video's durationSeconds by the per-
minute cost, add an AI image per scene for type: ai, and compare against
credits.tokens. An empty balance refuses the call with warning-no-credits.
Plan limits
GET /workspace returns the ceilings for the connected workspace. A limit the
plan does not cap is null, which means unlimited.
| Field | What it caps |
|---|---|
maxRendersPerMonth | Renders started in a rolling 30 day window. It caps generations over the same window too |
maxAutomations | Blueprints that can exist at once |
maxProfiles | Publishing profiles that can exist at once |
maxVideoDurationSeconds | How long a source video may be to run through generation |
maxRenderSeconds | How long a video may be to render |
storageBytes | Total stored bytes across the library. Deleting a video credits its bytes back |
watermark | true when renders on this plan carry a watermark |
Roughly, by plan:
| Plan | Renders and generations per month | Automations | Profiles | Max render length | Watermark | Publishing |
|---|---|---|---|---|---|---|
| Free | 3 | None | 1 | 3 minutes | Yes | No, download only |
| Starter | Uncapped | 1 | 1 | 10 minutes | No | Yes |
| Pro | Uncapped | 3 | 3 | 12 minutes | No | Yes |
| Advanced | Uncapped | 3 | 3 | 15 minutes | No | Yes |
Treat that table as orientation, not as a contract. GET /workspace is the live
answer for the workspace your key resolved to, including any storage ceiling, and
it is the only thing that stays right when plans change.
Long videos (videoDuration: "long", 8 to 15 minutes) need an Advanced plan and
are otherwise refused with warning-plan-duration. A workspace on a paid trial
can also run into a daily cap of 5 generations, which arrives as
warning-trial-daily-limit.
Current usage
GET /stats reports the counters against those ceilings, so you can throttle
yourself instead of discovering the limit on a refused call:
{
"data": {
"plan": "pro",
"videos": 37,
"automations": { "used": 2, "limit": 3 },
"clips": 118,
"generations": { "used": 24, "limit": null, "since": "2026-07-09T11:42:03.000Z" },
"renders": { "used": 19, "limit": null, "since": "2026-07-09T11:42:03.000Z" },
"storage": { "used": 3187441664, "limit": 50000000000 },
"credits": { "tokens": 42500000, "credits": 42500 },
"maxRenderSeconds": 720,
"maxDurationSeconds": 7200
}
}since is the start of the rolling window the generations and renders
counters cover. A limit of null means the plan does not cap that counter.
How a limit surfaces
A refusal reaches you in one of two shapes, with the same code in both.
Synchronously, when the call is refused before the job is queued, as a 400:
{
"error": {
"status": 400,
"code": "warning-plan-renders",
"message": "You have reached the limit of 3 renders per month on your current plan. Upgrade to render more videos."
}
}Asynchronously, when a running job hits it, on the job document:
process.error, render.error or publish.error, each carrying code,
message, blocked and action. The same object rides on the
video.generation_failed, video.render_failed and
video.publish_failed events.
blocked: true marks an expected limit rather than a crash, so a client can
render it as a quota instead of a red failure. action is the fix to offer:
| Code | Title | Action |
|---|---|---|
warning-no-credits | Out of credits | credits |
warning-trial-daily-limit | Daily trial limit reached | upgrade |
warning-plan-generations | Monthly generation limit reached | upgrade |
warning-plan-renders | Monthly render limit reached | upgrade |
warning-plan-duration | Video too long for your plan | upgrade |
warning-plan-render-time | Video too long to render on your plan | upgrade |
warning-plan-automations | Automation limit reached | upgrade |
warning-plan-publish | Publishing needs a paid plan | upgrade |
warning-source-not-ready | Source video is not ready | none |
warning-publish-not-configured | No publishing destination | configure |
warning-publish-no-accounts | No connected account | connect |
warning-publish-auth | Reconnect your social account | connect |
GET /reference/failure-codes returns exactly this list, so a client can render
an unfamiliar code by looking it up instead of falling back to a raw message.
One code sits outside that list: warning-plan-storage refuses a generation when
the workspace is at its storage ceiling. Free space by deleting videos, which
credits their bytes back to the quota.