Limits
Budgets that stop spending, per user, per entity, or per project.
A limit is a cap on cost or tokens over a window. Once it's hit, matching work is turned away instead of running. That is the difference between finding out about a runaway account now and finding out on the invoice.
Budgets are enforced twice: at submit, where a batch whose projected cost would breach a cap is rejected outright, and during the run, where individual lines start failing if a cap is reached mid-job. Rejecting at submit is the one you want, because a half-run batch is more work to reconcile than one that never started.
Requires the limits:read scope.
Create a limit
POST /v1/limits HTTP/1.1
Host: REGION.api.lararouter.com
Authorization: Bearer $LARAROUTER_API_KEY
Content-Type: application/json
{
"scope": "entity",
"entity": "customer:cus_2291",
"metric": "cost_usd",
"amount": 500,
"window": "month",
"description": "Acme monthly AI budget",
"notify_at": [0.8, 0.95]
}curl https://REGION.api.lararouter.com/v1/limits \
-H "Authorization: Bearer $LARAROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scope": "entity",
"entity": "customer:cus_2291",
"metric": "cost_usd",
"amount": 500,
"window": "month",
"description": "Acme monthly AI budget",
"notify_at": [0.8, 0.95]
}'Prop
Type
entity_type is the one that scales
scope: "entity_type" with entity_type: "user" gives every user their own cap, including users
who sign up tomorrow. One limit object instead of one per account, and nothing to remember at
signup.
Response
{
"id": "lim_4Kd9",
"object": "limit",
"scope": "entity",
"entity": "customer:cus_2291",
"metric": "cost_usd",
"amount": 500,
"window": "month",
"action": "block",
"description": "Acme monthly AI budget",
"notify_at": [0.8, 0.95],
"created": 1754006400
}List
GET /v1/limits?scope=entity&limit=20 HTTP/1.1
Host: REGION.api.lararouter.com
Authorization: Bearer $LARAROUTER_API_KEYCursor paginated, filterable by scope and entity.
curl -G https://REGION.api.lararouter.com/v1/limits \
-H "Authorization: Bearer $LARAROUTER_API_KEY" \
-d "scope=entity" \
-d "limit=20"{
"object": "list",
"data": [
{
"id": "lim_4Kd9",
"object": "limit",
"scope": "entity",
"entity": "customer:cus_2291",
"metric": "cost_usd",
"amount": 500,
"window": "month",
"action": "block"
}
],
"has_more": false,
"next_cursor": null
}Read current spend against a cap from GET /v1/usage/entities filtered to the same
entity. The list response is the cap itself, not a live meter.
{
"error": {
"type": "rate_limit_error",
"code": "usage_limit_exceeded",
"message": "Acme monthly AI budget of $500.00 has been reached. It resets on 1 September 2026.",
"limit_id": "lim_4Kd9",
"resets_at": 1756684800
}
}POST /v1/batches HTTP/1.1
Host: REGION.api.lararouter.com
Authorization: Bearer $LARAROUTER_API_KEY
Content-Type: application/json
{
"input_file_id": "file_9Hs4TnQ2",
"endpoint": "/v1/chat/completions",
"completion_window": "24h"
}
HTTP/1.1 429 Too Many Requests
Retry-After: 2534400At submit this is the response to POST /v1/batches and no job is created.
Where a cap is reached partway through a running job, the remaining lines land in error_file_id
with the same usage_limit_exceeded code, and whatever completed first is billed as normal.
This 429 is not a rate limit
It looks like one and shares the status code, but backing off doesn't help. Nothing frees up until
the window rolls over, which Retry-After will honestly tell you is four weeks away. Branch on
error.code and route this to the customer, not to your retry logic. See
Errors.
Handled properly, in Laravel:
namespace App\Actions\Lararouter;
use App\Exceptions\BudgetExhausted;
use Illuminate\Support\Facades\Http;
use Throwable;
class SubmitBatch
{
public function handle(string $inputFileId, string $entity): array
{
try {
$response = Http::withToken(config('services.lararouter.key'))
->withHeaders(['X-Lararouter-Entity' => $entity])
->post('https://REGION.api.lararouter.com/v1/batches', [
'input_file_id' => $inputFileId,
'endpoint' => '/v1/chat/completions',
'completion_window' => '24h',
]);
if ($response->json('error.code') === 'usage_limit_exceeded') {
// The customer's problem, not the on-call engineer's.
throw new BudgetExhausted(
message: $response->json('error.message'),
resetsAt: $response->json('error.resets_at'),
);
}
return $response->throw()->json();
} catch (Throwable $e) {
report($e);
throw $e;
}
}
}Because error.message is localized and includes the description you
wrote, it can go straight into the UI.
Warn before blocking
notify_at records the fractions of the cap that should warn before the block, for example
[0.8, 0.95]. action: "notify" records those thresholds without rejecting work, so you can measure
a cap before you enforce it.
{
"id": "lim_4Kd9",
"action": "notify",
"notify_at": [0.8, 0.95],
"amount": 500
}Watch spend on GET /v1/usage/entities for a fortnight, then create a second
limit with action: "block" when the threshold is right.