# Totalum App Builder API — Spending & Rate Limits

This file is the complete reference for this area. The short guide for agents is https://www.totalum.app/totalum-api.md.

- Base URL: `https://builder-api.totalum.app/api/v1/vcaas`
- Authentication: header `api-key: tlm_sk_…` on every request, from a server only.
- Response envelope: `{ "errors": null | { "errorCode", "errorMessage", "errorDetails?" }, "data": … }`.


---

# 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](https://www.totalum.app/docs/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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/credit-limits
cost: Free
```

The 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 `all` feature in [Feature Limits](#get-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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/credit-limits
```

:::success Success · 200 OK
```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

```endpoint
method: PATCH
path: /api/v1/vcaas/projects/:projectId/credit-limits
cost: Free
```

Set 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](https://www.totalum.app/docs/api/projects#launch-project), [Create Project](https://www.totalum.app/docs/api/projects#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:

1. **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.
2. **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](https://www.totalum.app/docs/limits#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](https://www.totalum.app/docs/api/webhooks) (`{ 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**

```bash
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-limits
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/credit-limits/at-limit
cost: Free
```

The 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](#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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/credit-limits/at-limit
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/limits
cost: Free
```

Every 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/limits
```

:::success Success · 200 OK
```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](https://www.totalum.app/docs/api/projects#release-runaway-pause) also brings them back.

## Set Feature Limits

```endpoint
method: PUT
path: /api/v1/vcaas/projects/:projectId/limits
cost: Free
```

Switch 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](#get-feature-limits) |

**Example request**

```bash
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/limits
```

:::success Success · 200 OK
```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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/rate-limit
cost: Free
```

How 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/rate-limit
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "perIpPerMinute": 5000, "allowlistCidrs": [], "verifiedBotsExempt": true, "planDefault": 5000, "planMax": 60000, "canDisable": true, "source": "plan" } }
```
:::

## Update Rate Limit

```endpoint
method: PATCH
path: /api/v1/vcaas/projects/:projectId/rate-limit
cost: Free
```

Change 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](#get-rate-limit) |

**Example request**

```bash
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-limit
```

:::success Success · 200 OK
```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 |
