Lararouter
API reference

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_KEY

Cursor 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: 2534400

At 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.

On this page