# 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) and on **infrastructure** credits (what the running app uses).
- **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.

**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.usedThisMonth` | object \| null | `{ development, infrastructure }` credits spent this month; `null` when it cannot be read right now |

**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 }, "usedThisMonth": { "development": 42.5, "infrastructure": 3.1 } } }
```
:::

## 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. Both 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)).

At the development cap a prompt is refused with `403 PROJECT_CREDIT_LIMIT_REACHED`, and a running prompt that crosses it stops (keeping what it built). At the infrastructure cap the app's paid built-in calls are refused until the next month or until you raise it. You get the `project.credit_limit.reached` [webhook](https://www.totalum.app/docs/api/webhooks) and an e-mail.

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

¹ At least one of the two.

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

**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 | Neither 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` |
| `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 |

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