# Track usage and cap conversations

**You will:** Set a budget of 500 conversations a month for a copilot, get an alert at 80%, and know what happens when it runs out.

**You need:**
- A key with usage:read, turns:read and budgets:write
- An agent that already answers messages
- A webhook endpoint, to receive the alert

The example is a support copilot inside your product. It may handle 500 conversations a month. At 400 you want a message in your channel. At 500 you want it to stop opening conversations, and you want your customers to reach a person.

A conversation is the unit Lelemon bills: a window per case of up to 10 agent turns or 24 hours, whichever comes first. The next turn opens another one. The model does not change the count.

**A budget: count, alert, cut, hand off**

A monthly budget of 500 conversations with an alert at 80%. Each new conversation counts once. At 80% an event fires once. At the limit the agent stops opening conversations and a person takes over.

1. Create the budget once. enforcement defaults to stop.
2. A contact writes and a turn starts. The first turn of a case, or the turn after 10 turns or 24 hours, opens a conversation.
3. When the turn opens a conversation, Lelemon counts it in every budget that applies. The turns that continue an open conversation count nothing. usedConversations grows.
4. The budget passes alertPercent and its state is alert. The agent keeps working.
5. The webhook budget.threshold_reached fires once. It fires again if you raise the limit and reach the new threshold.
6. The limit is reached. With enforcement stop the agent does not open another conversation; the ones already open keep going. With notify it would keep working and only the events would fire.
7. You write as a contact through the API.
8. Nothing is saved. Stop and do not retry until the month resets or you raise the limit with PATCH /v2/budgets/{id}.
9. A contact who writes to the agent after the cut is not left without an answer. This does not apply to internal copilots.
10. Lelemon hands the case to a person with transfer_to_human, reason budget_exceeded, and the contact reads a fixed message.
11. budget.exceeded fires once per month and budget, and case.escalated tells your team that a case needs a person.

## See where the conversations go

`GET /v2/usage` counts conversations, turns, replies and tokens per day, agent, model or subaccount. It shows no prices: use it to bill your own customers or to see where the volume is. The conversations are counted from the day Lelemon started counting them; earlier days show zero.

```bash title="cURL"
curl "https://api.lelemon.cl/v2/usage?from=2026-11-01&to=2026-11-30&groupBy=agent" \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
const usage = await lelemon.usage.retrieve({
  query: { from: '2026-11-01', to: '2026-11-30', groupBy: 'agent' },
});
```


```json title="Response"
{
  "object": "usage",
  "from": "2026-11-01",
  "to": "2026-11-30",
  "timezone": "America/Santiago",
  "totals": { "conversations": 312, "turns": 1240, "messages": 1198, "inputTokens": 3120400, "outputTokens": 211800 },
  "data": [
    {
      "agentId": "agt_7Hc2LmQ0x8bVw1TqS9mYp3",
      "conversations": 312,
      "turns": 1240,
      "messages": 1198,
      "inputTokens": 3120400,
      "outputTokens": 211800
    }
  ]
}
```


Days count in the time zone of the organization. The range cannot pass 366 days. A platform key reads one subaccount with the `Lelemon-Subaccount` header, or all of them with `groupBy=subaccount`. That grouping also counts the usage of the platform itself: it comes as the group with `subaccountId: null`, and `totals` is the sum of every group.

**Usage**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | enum | yes | `usage` |
| `from` | string | yes |  |
| `to` | string | yes |  |
| `timezone` | string | yes |  |
| `totals` | UsageTotals | yes |  |
| `data` | UsageGroup[] | yes |  |

Usage reports. It does not enforce. To enforce a limit, create a budget.

## Read what the agent did in a turn

`GET /v2/cases/{id}/turns` lists the turns of a case, newest first. Each turn has the tools the agent called, with their input and output, the tokens it spent and the model that answered.

```bash title="cURL"
curl "https://api.lelemon.cl/v2/cases/cse_4Zq0x8bVw1TqS9mYp3RkLd/turns?limit=5" \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
for await (const turn of lelemon.caseTurns.list('cse_4Zq0x8bVw1TqS9mYp3RkLd', { query: { limit: 5 } })) {
  console.log(turn.status, turn.tokens, turn.steps.map((step) => step.tool));
}
```


```json title="Response (one turn)"
{
  "object": "turn",
  "id": "trn_7Hc2LmQ0x8bVw1TqS9mYp3",
  "caseId": "cse_4Zq0x8bVw1TqS9mYp3RkLd",
  "status": "completed",
  "iterations": 2,
  "tokens": { "input": 2410, "output": 168 },
  "durationMs": 3120,
  "steps": [{ "tool": "find_order", "status": "ok", "durationMs": 420 }],
  "omittedSteps": 0
}
```


Input and output are masked by shape: emails, RUTs, phones, cards and secrets. A tool marked [noPersist](/guides/no-persist) leaves only its status and duration. Traces are kept for 90 days. To know when a turn ends without polling, subscribe to `case.turn_completed`.

**Turn**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | enum | yes | `turn` |
| `id` | string | yes |  |
| `caseId` | string | yes |  |
| `agentId` | string or null | yes |  |
| `model` | string or null | yes | The model that answered the last call of the turn. |
| `status` | enum | yes | completed: the agent finished by itself. incomplete: it stopped early and delivered a partial answer. failed: it gave no answer. `completed`, `incomplete`, `failed` |
| `stopReason` | string or null | yes |  |
| `iterations` | integer | yes | Calls to the model in the turn. |
| `tokens` | TurnTokens | yes |  |
| `durationMs` | integer | yes |  |
| `steps` | TurnStep[] | yes |  |
| `omittedSteps` | integer | yes | Tool calls that did not fit in steps (up to 50 steps and 64 KB are kept). |
| `createdAt` | string | yes |  |

## Measure how long cases take

`GET /v2/case-metrics` lists the cases that changed in a range, one row each, with the times you need for a service report: when the case came in, was handed to a person, was taken and was closed, who closed it, and the seconds spent with the agent and with a person. Rows come oldest change first, so you can sync them page by page into your own reports.

```bash title="cURL"
curl "https://api.lelemon.cl/v2/case-metrics?from=2026-11-02T13:00:00Z&to=2026-11-02T14:00:00Z&limit=50" \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
for await (const row of lelemon.caseMetrics.list({
  query: { from: '2026-11-02T13:00:00Z', to: '2026-11-02T14:00:00Z' },
})) {
  console.log(row.caseId, row.resolvedByAgent, row.waitSeconds);
}
```


```json title="Response"
{
  "object": "list",
  "data": [
    {
      "object": "case_metrics",
      "caseId": "cse_4Zq0x8bVw1TqS9mYp3RkLd",
      "pipelineId": "ppl_Q8dV1cX2mRw0TqS9mYp3Lk",
      "status": "closed",
      "control": "human",
      "createdAt": "2026-11-02T12:00:00.000Z",
      "escalatedAt": "2026-11-02T12:10:00.000Z",
      "takenAt": "2026-11-02T12:20:00.000Z",
      "takenBy": "mbr_4Zq0x8bVw1TqS9mYp3RkLd",
      "closedAt": "2026-11-02T12:50:00.000Z",
      "closedBy": "human",
      "resolvedByAgent": false,
      "waitSeconds": 600,
      "agentSeconds": 600,
      "humanSeconds": 1800,
      "updatedAt": "2026-11-02T12:50:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```


- `from` is required, as an ISO 8601 date and time with its zone. `to` is not included and defaults to now. It can be at most 31 days after `from`; a longer range answers `400` with `param: to`.
- The range filters by `updatedAt`, the last change of the case. A case that changes again moves to a later position, so a sync that stores the last `updatedAt` it saw misses nothing.
- Paginate with `cursor` and the same `from` and `to`. A cursor from another range answers `400 invalid_cursor`.
- `resolvedByAgent` is true when the agent closed the case without handing it off and without anyone taking it. `closedBy` is `agent`, `human` or `system` (inactivity, follow-ups exhausted), and `null` while the case is open.
- `waitSeconds` is the time between the last handoff and the first time a person took the case. It is `null` when that never happened.
- The scope is `cases:read`. A platform key reads one subaccount with the `Lelemon-Subaccount` header.
- The dashboard shows the same numbers in **Tiempos de los casos**, to owners and admins.

**CaseMetrics**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | enum | yes | `case_metrics` |
| `caseId` | string | yes |  |
| `pipelineId` | string | yes |  |
| `status` | enum | yes | `open`, `closed` |
| `control` | enum | yes | Who has the case now. human also covers a case the agent handed off and nobody took yet. `agent`, `human` |
| `createdAt` | string | yes |  |
| `escalatedAt` | string or null | yes | The first time the agent handed the case to a person. |
| `takenAt` | string or null | yes | The first time a person took the case. |
| `takenBy` | string or null | yes | The member who took it first. null when nobody did, or when it was taken from the phone. |
| `closedAt` | string or null | yes | When it was closed. null while it is open. |
| `closedBy` | enum | yes | Who closed it: the agent, a person, or the system (inactivity, follow-ups exhausted). null while it is open. `agent`, `human`, `system`, `null` |
| `resolvedByAgent` | boolean | yes | True when the agent closed it without handing it off and without anyone taking it. |
| `waitSeconds` | integer or null | yes | Seconds between the last handoff and the first time a person took the case. null when it was never handed off and taken. |
| `agentSeconds` | integer | yes | Seconds the agent attended the case. |
| `humanSeconds` | integer | yes | Seconds a person attended the case. |
| `updatedAt` | string | yes | The last change. It is the field the range filters by. |

## Create the budget

```bash title="cURL"
curl https://api.lelemon.cl/v2/budgets \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: budget-copilot-2026" \
  --json '{ "scope": "organization", "monthlyLimitConversations": 500, "alertPercent": 80 }'
```

```ts title="Node.js"
const budget = await lelemon.budgets.create({
  scope: 'organization',
  monthlyLimitConversations: 500,
  alertPercent: 80,
});
```


```json title="Response"
{
  "object": "budget",
  "id": "bgt_7Hc2LmQ0x8bVw1TqS9mYp3",
  "scope": "organization",
  "subjectId": null,
  "monthlyLimitConversations": 500,
  "alertPercent": 80,
  "enforcement": "stop",
  "usedConversations": 0,
  "state": "ok",
  "period": { "start": "2026-11-01", "resetsAt": "2026-12-01T03:00:00.000Z" }
}
```


**Budget**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | enum | yes | `budget` |
| `id` | string | yes |  |
| `scope` | enum | yes | What the budget covers: the whole organization, one subaccount, one agent or one contact. `organization`, `subaccount`, `agent`, `contact` |
| `subjectId` | string or null | yes | The sub_, agt_ or ctc_ id the budget covers. null for the organization. |
| `monthlyLimitConversations` | integer | yes | Monthly limit in conversations. |
| `alertPercent` | integer | yes | Percent of the limit that fires budget.threshold_reached. |
| `enforcement` | enum | yes | stop: at the limit the agent stops and the API answers 402 budget_exceeded. notify: the agent keeps working and only the events fire. `stop`, `notify` |
| `usedConversations` | integer | yes | Conversations used this month. |
| `state` | enum | yes | ok: under the alert percent. alert: past it. limit_reached: the used conversations reached the limit. `ok`, `alert`, `limit_reached` |
| `period` | BudgetPeriod | yes |  |
| `createdAt` | string | yes |  |
| `updatedAt` | string | yes |  |

A budget covers one `scope`: the whole `organization`, one `subaccount`, one `agent` or one `contact`. Set `subjectId` for every scope but `organization`. A budget per `contact` keeps one stranger from using up the budget of everyone. Several budgets can apply to the same turn, and the strictest one stops it. You get one budget per scope and subject: a second one answers `409`. Creating a budget is not idempotent, so keep the `Idempotency-Key` stable when you retry.

The month is counted in the time zone of the organization. A turn that opens a conversation counts one in every budget that applies. A turn that continues an open conversation counts nothing, so a budget at its limit never cuts a conversation in the middle.

## React to the alert

At `alertPercent` the webhook `budget.threshold_reached` fires once per month and budget. Its `data` has `usedConversations`, `monthlyLimitConversations`, `alertPercent` and `resetsAt`.

```json title="data of budget.threshold_reached"
{
  "budget": { "id": "bgt_7Hc2LmQ0x8bVw1TqS9mYp3", "scope": "organization", "subjectId": null },
  "alertPercent": 80,
  "monthlyLimitConversations": 500,
  "usedConversations": 400,
  "resetsAt": "2026-12-01T03:00:00.000Z"
}
```


The agent keeps working. Use the alert to decide: raise the limit, or let it run out.

## What happens at the limit

With `enforcement: "stop"`, the default:

- `POST /v2/cases/{id}/messages` with `author: contact` answers `402 budget_exceeded` when the message would open a new conversation. Nothing is saved. Do not retry until the month resets or you raise the limit. A case whose conversation is already open keeps answering.
- A contact who writes to the agent gets a fixed message, and the case goes to a person (`transfer_to_human`, reason `budget_exceeded`). You get `case.escalated`.
- The webhook `budget.exceeded` fires once per month and budget.

With `enforcement: "notify"` the agent never stops and only the events fire.

Read [`budget_exceeded`](/errors/budget_exceeded) for the error.

## Raise the limit

```bash title="cURL"
curl -X PATCH https://api.lelemon.cl/v2/budgets/bgt_7Hc2LmQ0x8bVw1TqS9mYp3 \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "monthlyLimitConversations": 750 }'
```

```ts title="Node.js"
await lelemon.budgets.update('bgt_7Hc2LmQ0x8bVw1TqS9mYp3', { monthlyLimitConversations: 750 });
```


Raising the limit above what is used makes a stopped agent answer again at once and re-arms the alert. The `scope` and the `subjectId` cannot change: create another budget instead. The same body twice leaves the same budget.

A key of a subaccount only reaches the budgets of its own organization. The budget its platform set for it is not visible to it and cannot be raised from there.

## Delete a budget

```bash title="cURL"
curl -X DELETE https://api.lelemon.cl/v2/budgets/bgt_7Hc2LmQ0x8bVw1TqS9mYp3 \
  -H "Authorization: Bearer $LELEMON_API_KEY"
```

```ts title="Node.js"
await lelemon.budgets.delete('bgt_7Hc2LmQ0x8bVw1TqS9mYp3');
```


The answer is `204`. An agent that the budget was stopping answers again at once, its alerts stop and the conversations counted so far go with it; the usage report is not affected. To cap the same scope and subject again, create a new budget. Deleting it a second time answers `404`.
