Track usage and cap conversations
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.
- Create the budget once. enforcement defaults to stop.
- 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.
- 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.
- The budget passes alertPercent and its state is alert. The agent keeps working.
- The webhook budget.threshold_reached fires once. It fires again if you raise the limit and reach the new threshold.
- 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.
- You write as a contact through the API.
- Nothing is saved. Stop and do not retry until the month resets or you raise the limit with PATCH /v2/budgets/{id}.
- A contact who writes to the agent after the cut is not left without an answer. This does not apply to internal copilots.
- Lelemon hands the case to a person with transfer_to_human, reason budget_exceeded, and the contact reads a fixed message.
- 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.
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.
Usageobject, 6 fields
objectenumrequiredusagefromstringrequiredtostringrequiredtimezonestringrequiredtotalsUsageTotalsrequiredUsageTotals6 fields
turnsintegerrequiredTurns the agent took.
messagesintegerrequiredReplies the agent wrote.
inputTokensintegerrequiredoutputTokensintegerrequiredconversationsintegerrequiredConversations opened in the range, the unit that is billed. A conversation is a window per case of up to 10 agent turns or 24 hours, whichever comes first; the next turn opens another one.
casesintegerrequiredCases (conversations) in which the agent wrote at least one reply in the range. A case counts once however many replies it has.
dataUsageGroup[]requiredUsageGroup10 fields
turnsintegerrequiredTurns the agent took.
messagesintegerrequiredReplies the agent wrote.
inputTokensintegerrequiredoutputTokensintegerrequiredconversationsintegeroptionalConversations opened in this group. Absent with groupBy=model: a conversation is not split by model.
casesintegeroptionalCases in which the agent wrote a reply in this group. A case with replies on two days counts in both, so the groups can add up to more than totals.cases. Absent with groupBy=model: a case is not split by model.
daystringoptionalagentIdstringoptionalmodelstringoptionalsubaccountIdstring or nulloptionalWith groupBy=subaccount. null is the usage of the platform itself, which is not in any subaccount.
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.
Input and output are masked by shape: emails, RUTs, phones, cards and secrets. A tool marked noPersist 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.
Turnobject, 13 fields
objectenumrequiredturnidstringrequiredcaseIdstringrequiredagentIdstring or nullrequiredmodelstring or nullrequiredThe model that answered the last call of the turn.
statusenumrequiredcompleted: the agent finished by itself. incomplete: it stopped early and delivered a partial answer. failed: it gave no answer.
completedincompletefailedstopReasonstring or nullrequirediterationsintegerrequiredCalls to the model in the turn.
tokensTurnTokensrequiredTurnTokens2 fields
inputintegerrequiredoutputintegerrequired
durationMsintegerrequiredstepsTurnStep[]requiredTurnStep5 fields
toolstringrequiredName of the tool the agent called. A Lelemon system tool shows its public name (transfer_to_human, save_case_data, send_buttons...), the same one GET /v2/agents/{id}/tools lists. For a tool from an MCP server it is the server slug and the tool name.
statusenumrequiredokerrordurationMsintegerrequiredMilliseconds the tool took.
inputobjectoptionalWhat the agent sent to the tool, with emails, RUTs, phones, cards and secrets masked by shape. Absent when the tool is marked noPersist: those tools leave only status and duration.
outputstringoptionalWhat the tool answered, masked the same way and cut at 4000 characters. Absent when the tool is marked noPersist.
omittedStepsintegerrequiredTool calls that did not fit in steps (up to 50 steps and 64 KB are kept).
createdAtstring (date-time)required
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.
fromis required, as an ISO 8601 date and time with its zone.tois not included and defaults to now. It can be at most 31 days afterfrom; a longer range answers400withparam: 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 lastupdatedAtit saw misses nothing. - Paginate with
cursorand the samefromandto. A cursor from another range answers400 invalid_cursor. resolvedByAgentis true when the agent closed the case without handing it off and without anyone taking it.closedByisagent,humanorsystem(inactivity, follow-ups exhausted), andnullwhile the case is open.waitSecondsis the time between the last handoff and the first time a person took the case. It isnullwhen that never happened.- The scope is
cases:read. A platform key reads one subaccount with theLelemon-Subaccountheader. - The dashboard shows the same numbers in Tiempos de los casos, to owners and admins.
CaseMetricsobject, 16 fields
objectenumrequiredcase_metricscaseIdstringrequiredpipelineIdstringrequiredstatusenumrequiredopenclosedcontrolenumrequiredWho has the case now. human also covers a case the agent handed off and nobody took yet.
agenthumancreatedAtstring (date-time)requiredescalatedAtstring or null (date-time)requiredThe first time the agent handed the case to a person.
takenAtstring or null (date-time)requiredThe first time a person took the case.
takenBystring or nullrequiredThe member who took it first. null when nobody did, or when it was taken from the phone.
closedAtstring or null (date-time)requiredWhen it was closed. null while it is open.
closedByenumrequiredWho closed it: the agent, a person, or the system (inactivity, follow-ups exhausted). null while it is open.
agenthumansystemnullresolvedByAgentbooleanrequiredTrue when the agent closed it without handing it off and without anyone taking it.
waitSecondsinteger or nullrequiredSeconds between the last handoff and the first time a person took the case. null when it was never handed off and taken.
agentSecondsintegerrequiredSeconds the agent attended the case.
humanSecondsintegerrequiredSeconds a person attended the case.
updatedAtstring (date-time)requiredThe last change. It is the field the range filters by.
Create the budget
Budgetobject, 12 fields
objectenumrequiredbudgetidstringrequiredscopeenumrequiredWhat the budget covers: the whole organization, one subaccount, one agent or one contact.
organizationsubaccountagentcontactsubjectIdstring or nullrequiredThe sub_, agt_ or ctc_ id the budget covers. null for the organization.
monthlyLimitConversationsintegerrequiredMonthly limit in conversations.
alertPercentintegerrequiredPercent of the limit that fires budget.threshold_reached.
enforcementenumrequiredstop: at the limit the agent stops and the API answers 402 budget_exceeded. notify: the agent keeps working and only the events fire.
stopnotifyusedConversationsintegerrequiredConversations used this month.
stateenumrequiredok: under the alert percent. alert: past it. limit_reached: the used conversations reached the limit.
okalertlimit_reachedperiodBudgetPeriodrequiredBudgetPeriod2 fields
startstringrequiredFirst day of the month.
resetsAtstring (date-time)requiredWhen the month ends and the spent amount goes back to zero.
createdAtstring (date-time)requiredupdatedAtstring (date-time)required
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.
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}/messageswithauthor: contactanswers402 budget_exceededwhen 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, reasonbudget_exceeded). You getcase.escalated. - The webhook
budget.exceededfires once per month and budget.
With enforcement: "notify" the agent never stops and only the events fire.
Read budget_exceeded for the error.
Raise the limit
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
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.