Spending & Rate Limits
Keep every project's spending and traffic under control.
Three independent controls, all per project. Every limit of an account and a project, and which ones Totalum can raise, is on Limits.
- Credit limits — a monthly cap on development credits (prompts, publishes, builder actions), on infrastructure credits (what the running app uses), and on the development environment's infrastructure (the preview, its database and its scheduled jobs; 100 credits a month unless you change it).
- Feature limits — switch each built-in feature of the app (AI, e-mail, web scraping…) on or off and give it its own monthly budget.
- Rate limit — requests per minute each visitor (IP) may send to the published site.
A refused call never reaches a provider and costs nothing. Budgets reset on the 1st of each month (UTC).
#Get Credit Limits
/api/v1/vcaas/projects/:projectId/credit-limitsapi-keyFreeThe project's monthly caps and what it spent this month (UTC), per category — the same figures the caps are enforced on:
- development — everything the AI agent's prompts and the project's builder actions (publishes, rebuilds, restores, exports, code edits…) were charged this month;
- infrastructure — what the running app used this month: hosting, database and every built-in feature (the total of the
allfeature in Feature Limits); - developmentInfrastructure — the part of it used by the development environment: the preview, the development database, the preview's scheduled jobs and the built-in calls made from the preview (never the AI agent's own prompts, which are development credits).
All start again at 0 on the 1st of each month, 00:00 UTC.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.creditLimits.maxDevelopmentCreditsPerMonth | number | null | The development cap; null = no cap |
data.creditLimits.maxInfrastructureCreditsPerMonth | number | null | The infrastructure cap; null = no cap |
data.creditLimits.maxDevInfrastructureCreditsPerMonth | number | null | The development environment's cap (default 100); null = no cap. Absent when it cannot be read right now |
data.usedThisMonth | object | null | { development, infrastructure, developmentInfrastructure } credits spent this month; null when it cannot be read right now |
data.devInfrastructureStopped | boolean | true while the development environment is stopped at its cap |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/credit-limitsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/credit-limits", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "creditLimits": { "maxDevelopmentCreditsPerMonth": 5000, "maxInfrastructureCreditsPerMonth": 300, "maxDevInfrastructureCreditsPerMonth": 100 }, "usedThisMonth": { "development": 42.5, "infrastructure": 3.1, "developmentInfrastructure": 0.4 }, "devInfrastructureStopped": false } }#Update Credit Limits
/api/v1/vcaas/projects/:projectId/credit-limitsapi-keyFreeSet the project's monthly caps. null removes a cap. Defaults for a project created through the API: a development cap of 5,000 credits a month and an infrastructure cap of 300 credits a month; every project's development environment has a cap of 100 credits a month. The first two can also be set when the project is created (Launch Project, Create Project).
Values: a cap is a number above 0, or null for no cap; 0 or a negative number is refused with 400 INVALID_LIMIT. The infrastructure caps (the app's and the development environment's) are applied as the whole number at or above the value, at least 1: 0.4 is applied as 1 and 12.5 as 13. The development cap is applied exactly as sent.
At the development cap a new prompt, and any paid builder action (a publish, a rebuild…), is refused with 403 PROJECT_CREDIT_LIMIT_REACHED and costs nothing. A prompt already running when its charges cross the cap is stopped: it finishes the step in progress, wraps up so that what it built works, and ends with PROJECT_CREDIT_LIMIT_REACHED. It can go past the cap by the step that crossed it plus at most 5 credits, and every credit it used is charged.
At the infrastructure cap (it counts hosting, database, files, AI, e-mail and every other built-in) there are two steps, each for that project only:
- At the cap the app's built-in features are refused with
429 FEATURE_LIMIT_REACHED(errorDetails.feature=all): AI, e-mail, file uploads, web scraping and search, browser sessions, document scanning, PDFs and screenshots, speech, real-time rooms, vectors, connected accounts, and creating or changing scheduled jobs. The site, its database, hosting and stored files keep running. - At the cap plus a reserve of 10 % of the cap, at least 5 credits (300 → 330, 20 → 25, 2 → 7) the project is paused: its site and preview show a paused page and its API keys answer
429 PROJECT_CREDIT_LIMIT_REACHED(errorDetails:category: "infrastructure",limit,pauseAt).
Raising the cap re-checks both at once: the pause lifts when the new cap plus its reserve is above this month's spend, and the built-in features come back when the new cap is above it. Removing the cap (null) lifts both; both also end at the start of next month (UTC). The reserve is fixed; it cannot be changed.
At the development environment's cap (maxDevInfrastructureCreditsPerMonth, 100 credits a month by default; it counts the preview, the development database, the preview's scheduled jobs and the built-in calls made from the preview) the development environment of that project stops: the preview shows a paused page, the preview's scheduled jobs are skipped, and built-in calls made from the development environment answer 429 DEV_INFRASTRUCTURE_LIMIT_REACHED. The published site, its database and its scheduled jobs keep running, and the AI agent can still work on the project. It runs again when:
- you send the agent a prompt: the development environment gets one more full cap from this month's spend (with a cap of 100 and 104 spent, it stops again at 204);
- you raise the cap above this month's development-environment spend, or remove it (
null); - the next month starts (UTC).
A sudden jump in database usage is charged like any other usage and is not paused on its own: you get an alert (Unusual database usage) and it continues until the infrastructure cap plus its reserve pauses the project. Set one on every project.
The first time a project reaches a cap in a month you get the project.credit_limit.reached webhook ({ projectId, category, spent, limit }) and an e-mail — once per project, per cap and per month, however many requests are refused. The infrastructure cap tells you at each step, once per step and month: the payload adds stage ("builtins_stopped" at the cap, "project_paused" at the pause) and pauseAt (the spend that pauses the project). The development environment's cap sends category: "development_infrastructure", stage: "dev_stopped" and stopAt (the spend that stopped it), once a month; a later stop in the same month is told in the project's chat. A new cap applies from the next prompt; a prompt already running keeps the cap it started with.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
maxDevelopmentCreditsPerMonth | number | null | No¹ | Positive number, or null for no cap |
maxInfrastructureCreditsPerMonth | number | null | No¹ | Positive number, or null for no cap |
maxDevInfrastructureCreditsPerMonth | number | null | No¹ | The development environment's cap: positive number, or null for no cap |
¹ At least one of the three.
Response fields
| Field | Type | Description |
|---|---|---|
data.creditLimits.maxDevelopmentCreditsPerMonth | number | null | When sent: the new development cap |
data.creditLimits.maxInfrastructureCreditsPerMonth | number | null | When sent: the new infrastructure cap |
data.creditLimits.maxDevInfrastructureCreditsPerMonth | number | null | When sent: the development environment's cap as applied (a whole number, at least 1) |
Example request
curl -X PATCH \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"maxDevelopmentCreditsPerMonth":500,"maxInfrastructureCreditsPerMonth":null}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/credit-limitsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/credit-limits", {
method: "PATCH",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"maxDevelopmentCreditsPerMonth": 500,
"maxInfrastructureCreditsPerMonth": null
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "creditLimits": { "maxDevelopmentCreditsPerMonth": 500, "maxInfrastructureCreditsPerMonth": null } } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
MISSING_LIMIT_FIELDS | 400 | No cap was sent |
INVALID_CREDIT_LIMITS | 400 | The body is not an object |
INVALID_LIMIT | 400 | A cap is not a positive number or null (0 included) |
OWNER_ONLY | 403 | Only the account owner can change the caps |
#List Projects at Their Limit
/api/v1/vcaas/projects/credit-limits/at-limitapi-keyFreeThe account's projects that reached a monthly credit limit this month (UTC), with their caps and spend — the list behind the platform's "projects at their monthly limit" notice. Raise one with Update Credit Limits.
Response fields
| Field | Type | Description |
|---|---|---|
data.projects | object[] | The projects at a limit (empty when none) |
data.projects[].projectId | string | The project id |
data.projects[].label | string | null | Its display name |
data.projects[].createdVia | string | null | "ui" (the platform) or "api" |
data.projects[].creditLimits | object | { maxDevelopmentCreditsPerMonth, maxInfrastructureCreditsPerMonth } |
data.projects[].usedThisMonth | object | { development, infrastructure } credits spent this month |
data.projects[].atLimit | object | { development, infrastructure }: true for each limit reached |
data.resetsAt | string | When the limits reset (the start of next month, UTC) |
data.canEdit | boolean | Whether this caller may raise them (the account owner) |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/credit-limits/at-limitconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/credit-limits/at-limit", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "projects": [{ "projectId": "barber-bookings", "label": "Barber bookings", "createdVia": "api", "creditLimits": { "maxDevelopmentCreditsPerMonth": 5000, "maxInfrastructureCreditsPerMonth": 300 }, "usedThisMonth": { "development": 812.4, "infrastructure": 300.2 }, "atLimit": { "development": false, "infrastructure": true } }], "resetsAt": "2026-11-01T00:00:00.000Z", "canEdit": true } }#Get Feature Limits
/api/v1/vcaas/projects/:projectId/limitsapi-keyFreeEvery built-in feature of the app: whether it is on, its monthly budgets and what it used this month. Every feature is on by default, with no budget of its own.
| Feature | Covers |
|---|---|
ai | Every AI call below |
ai_text | Chat, structured output, vision, embeddings, reranking |
ai_image | Image generation and editing |
ai_video | Video generation |
speech | Speech-to-text and text-to-speech |
web | Web scraping, crawling, search, extraction |
browser | Cloud browser sessions |
scan | Document scanning (OCR) |
storage | File uploads and stored files |
email | Sending e-mail |
pdf | PDFs, screenshots, SEO pings |
vectors | Vector search |
realtime | Real-time rooms |
cron | Scheduled job runs |
mobile | Mobile app builds |
database | The database (cannot be switched off) |
worker | The site itself (cannot be switched off) |
all | Everything: one switch and one total monthly budget, in credits (the infrastructure cap: spent, the built-in features stop; plus its reserve, the project is paused) |
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.plan | string | The account's plan |
data.features | object | Feature → its entry |
data.features.<feature>.enabled | boolean | false = switched off |
data.features.<feature>.parent | string | undefined | "ai" for ai_text, ai_image, ai_video (the parent's switch and budget also apply) |
data.features.<feature>.monthlyCreditCap | number | undefined | Budget in credits, when set |
data.features.<feature>.monthlyUnitCap | number | undefined | Budget in the feature's unit, when set |
data.features.<feature>.unit | string | calls, recipients, pages, bytes, session_minutes, room_minutes, executions, builds, rows, requests or credits |
data.features.<feature>.consumed | object | { credits, units } this month |
data.features.<feature>.period | object | { from, to }: the month |
data.features.<feature>.planCeiling | object | Highest budget your plan allows: { monthlyCreditCap, monthlyUnitCap } (null = none) |
data.features.<feature>.source | string | plan (default), user (you set it) or ceo (set by Totalum support) |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/limitsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/limits", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{
"errors": null,
"data": {
"plan": "starter",
"features": {
"ai_video": { "enabled": false, "parent": "ai", "unit": "calls", "consumed": { "credits": 0, "units": 0 }, "period": { "from": "2026-10-01T00:00:00.000Z", "to": "2026-11-01T00:00:00.000Z" }, "planCeiling": { "monthlyCreditCap": null, "monthlyUnitCap": 20 }, "source": "user" },
"all": { "enabled": true, "monthlyCreditCap": 300, "unit": "credits", "consumed": { "credits": 12.4, "units": 0 }, "period": { "from": "2026-10-01T00:00:00.000Z", "to": "2026-11-01T00:00:00.000Z" }, "planCeiling": { "monthlyCreditCap": null, "monthlyUnitCap": null }, "source": "user" }
}
}
}The answer lists every feature; the example shows two.
Concurrency. Besides budgets, two features have per-project concurrency limits: browser (3 open sessions, 1 on the free plan; a call up to 60 s) and web (1 call at a time, up to 10 s). A call over the limit waits in a fair queue, then answers 429 CONCURRENCY_QUEUE_TIMEOUT (or CONCURRENCY_QUEUE_FULL) with errorDetails.retryAfter. A project makes at most 100 built-in calls per second in total; above it a call answers 429 RATE_LIMITED with Retry-After.
Busy hours. When an app spends half of what would trigger the emergency stop within one hour, its image, video and speech AI calls answer 429 AI_MEDIA_PAUSED_THIS_HOUR until that hour ends (UTC), with Retry-After; text AI and everything else keep working. They come back by themselves at the next hour; releasing an emergency-stop pause also brings them back.
#Set Feature Limits
/api/v1/vcaas/projects/:projectId/limitsapi-keyFreeSwitch features on or off and set monthly budgets. Send only the features you change; null removes a budget. Account owner only. Setting the budget of all also sets the project's infrastructure credit limit.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
features | object | Yes | Feature → { enabled?, monthlyCreditCap?, monthlyUnitCap? } |
Response fields
| Field | Type | Description |
|---|---|---|
data.plan | string | The account's plan |
data.features | object | Every feature after the change, as in Get Feature Limits |
Example request
curl -X PUT \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"features":{"ai_video":{"enabled":false},"ai":{"monthlyCreditCap":200},"all":{"monthlyCreditCap":500}}}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/limitsconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/limits", {
method: "PUT",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"features": {
"ai_video": {
"enabled": false
},
"ai": {
"monthlyCreditCap": 200
},
"all": {
"monthlyCreditCap": 500
}
}
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "plan": "starter", "features": { "ai_video": { "enabled": false, "parent": "ai", "unit": "calls", "consumed": { "credits": 0, "units": 0 }, "period": { "from": "2026-10-01T00:00:00.000Z", "to": "2026-11-01T00:00:00.000Z" }, "planCeiling": { "monthlyCreditCap": null, "monthlyUnitCap": 20 }, "source": "user" } } } }What the app's code gets when a feature is refused (show a message, do not retry):
| App error | HTTP | Meaning |
|---|---|---|
FEATURE_DISABLED | 403 | The feature (or ai, or all) is switched off; errorDetails.feature names it |
FEATURE_LIMIT_REACHED | 429 | Its budget is spent; errorDetails: feature, limit, used, unit, resetsAt |
AI_MEDIA_PAUSED_THIS_HOUR | 429 | The app used a lot of credits this hour, so image, video and speech AI wait until the hour ends (UTC); Retry-After and errorDetails.resetsAt say when. Text AI and everything else keep working |
Error codes
| Code | HTTP | Meaning |
|---|---|---|
UNKNOWN_FEATURE | 400 | Not a feature of the table above |
INVALID_LIMIT | 400 | A negative or non-integer budget, or a monthlyUnitCap on all |
FEATURE_NOT_DISABLABLE | 400 | database and worker cannot be switched off |
LIMIT_ABOVE_PLAN_CEILING | 400 | A budget above your plan's ceiling |
OWNER_ONLY | 403 | Only the account owner can change budgets |
#Get Rate Limit
/api/v1/vcaas/projects/:projectId/rate-limitapi-keyFreeHow many requests per minute each visitor (IP) may send to the published site.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Response fields
| Field | Type | Description |
|---|---|---|
data.perIpPerMinute | number | null | Requests per minute per IP; null = off |
data.allowlistCidrs | string[] | IP ranges that are never limited (up to 50) |
data.verifiedBotsExempt | boolean | Search engines and other verified bots are not limited |
data.planDefault | number | Your plan's default |
data.planMax | number | null | The highest value your plan allows |
data.canDisable | boolean | Whether your plan may switch it off |
data.source | string | "plan" or "user" |
Example request
curl -H "api-key: tlm_sk_your_key" \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/rate-limitconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/rate-limit", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "perIpPerMinute": 5000, "allowlistCidrs": [], "verifiedBotsExempt": true, "planDefault": 5000, "planMax": 60000, "canDisable": true, "source": "plan" } }#Update Rate Limit
/api/v1/vcaas/projects/:projectId/rate-limitapi-keyFreeChange the per-visitor limit, the allowlist or the bot exemption. Send only what changes.
Separately, each visitor may cause at most 20 paid built-in calls a minute (AI, web, browser, screenshots), so nobody can spend the project's credits by calling a public AI page in a loop; over it the app's call answers 429 RATE_LIMIT_EXCEEDED with Retry-After. Ask the agent to change it ("let each visitor send 60 AI messages a minute").
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
perIpPerMinute | number | null | No | From 10 to 60,000 on paid plans (5,000 on the free plan, the default everywhere); null = off (paid plans only) |
allowlistCidrs | string[] | No | Up to 50 IP ranges (203.0.113.0/24) |
verifiedBotsExempt | boolean | No | Exempt verified bots |
Response fields
| Field | Type | Description |
|---|---|---|
data | object | The setting after the change, as in Get Rate Limit |
Example request
curl -X PATCH \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"perIpPerMinute":300}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/rate-limitconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/rate-limit", {
method: "PATCH",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"perIpPerMinute": 300
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "perIpPerMinute": 300, "allowlistCidrs": [], "verifiedBotsExempt": true, "planDefault": 5000, "planMax": 60000, "canDisable": true, "source": "user" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR | 400 | A value is out of range |
RATE_LIMIT_ABOVE_PLAN | 400 | Above your plan's maximum (errorDetails.planMax) |
INVALID_CIDR | 400 | An allowlist entry is not an IPv4 or IPv6 range |
RATE_LIMIT_DISABLE_NOT_ALLOWED | 403 | The free plan cannot switch the limit off |
