Lararouter
Getting started

Quickstart

Upload a batch, collect the results, and read back what it cost.

Five steps. By the end you'll have run a real batch, attributed each line to a user, and pulled the cost back out of the billing API.

Every invocation on Lararouter runs as a batch. You upload a JSONL file, create a job against it, and collect results inside the completion window: 24 hours on Pay-as-you-go, 4 hours on Enterprise.

Check your key

GET /v1/me is the cheapest possible sanity check. One call proves your key is valid, tells you which region answered, and confirms which plan and completion window you're on.

curl https://REGION.api.lararouter.com/v1/me \
  -H "Authorization: Bearer $LARAROUTER_API_KEY"
{
  "object": "identity",
  "key": { "id": "key_7Fq2", "name": "production", "last4": "9c41", "scopes": ["batches:write", "files:write", "usage:read"] },
  "project": { "id": "prj_2mK9", "name": "Acme API", "plan": "pay_as_you_go" },
  "region": "REGION",
  "permissions": ["batches:write", "files:write", "usage:read"],
  "retention": { "requests_days": 90, "conversations_days": 90 },
  "batches": { "completion_window": "24h", "concurrent": 8, "max_lines_per_batch": 50000 }
}

If this returns 401, the key is wrong or revoked. See Authentication.

Write the input file

One JSON object per line, no wrapping array. body is exactly the request an OpenAI-compatible client would have sent, so anything you already build for chat completions drops in unchanged.

requests.jsonl
{"custom_id":"ticket_4821","method":"POST","url":"/v1/chat/completions","body":{"model":"llama-3.3-70b","messages":[{"role":"user","content":"Summarize this ticket."}]},"metadata":{"entity":"user:usr_8421","tags":"feature=support-summary"}}
{"custom_id":"ticket_4822","method":"POST","url":"/v1/chat/completions","body":{"model":"llama-3.3-70b","messages":[{"role":"user","content":"Summarize this ticket."}]},"metadata":{"entity":"user:usr_9013","tags":"feature=support-summary"}}

custom_id is yours and is how you match a result back to the row that produced it. metadata is what turns "we spent $4,000 on inference" into "this customer cost us $12.40 last month". See Attribution.

Now upload it with a purpose of batch.

curl https://REGION.api.lararouter.com/v1/files \
  -H "Authorization: Bearer $LARAROUTER_API_KEY" \
  -F purpose=batch \
  -F file=@requests.jsonl
{
  "id": "file_9Hs4TnQ2",
  "object": "file",
  "purpose": "batch",
  "line_count": 2,
  "status": "processed"
}

Wait for status to read processed before moving on. See Files for the size ceilings and validation rules.

Create the batch

Hand over the file ID and say which endpoint the lines target.

curl https://REGION.api.lararouter.com/v1/batches \
  -H "Authorization: Bearer $LARAROUTER_API_KEY" \
  -H "Idempotency-Key: summaries-2026-08-03" \
  -H "Content-Type: application/json" \
  -d '{
    "input_file_id": "file_9Hs4TnQ2",
    "endpoint": "/v1/chat/completions",
    "completion_window": "24h"
  }'
{
  "id": "batch_5Rj8Lm2Q",
  "object": "batch",
  "status": "validating",
  "request_counts": { "total": 2, "completed": 0, "failed": 0 },
  "cost_usd": 0,
  "expires_at": 1754236800
}

Always send an idempotency key

A retried create without one submits the file a second time and bills it twice. See Idempotency.

Collect the results

Poll GET /v1/batches/{id} on a schedule. A minute between checks is plenty for a job measured in hours.

curl https://REGION.api.lararouter.com/v1/batches/batch_5Rj8Lm2Q \
  -H "Authorization: Bearer $LARAROUTER_API_KEY"

curl https://REGION.api.lararouter.com/v1/files/file_2Wp7Kd91/content \
  -H "Authorization: Bearer $LARAROUTER_API_KEY" \
  -o results.jsonl
{"custom_id":"ticket_4821","response":{"status_code":200,"request_id":"req_01JD8XK4M2","body":{"choices":[{"message":{"role":"assistant","content":"The customer is locked out following a password reset…"},"finish_reason":"stop"}],"usage":{"prompt_tokens":284,"completion_tokens":96,"cost_usd":0.00042}}},"error":null}

Lines fail individually, so a completed batch can still have a non-zero request_counts.failed. Read error_file_id as well as output_file_id. Batches covers every status.

Use the Billing API

Now ask what that user has spent. The same endpoint answers for one entity or for the intersection of two.

curl -G https://REGION.api.lararouter.com/v1/usage/entities \
  -H "Authorization: Bearer $LARAROUTER_API_KEY" \
  -d "entity=user:usr_8421" \
  -d "start=2026-08-01" \
  -d "group_by=model"
{
  "object": "usage",
  "entities": ["user:usr_8421"],
  "totals": { "tokens_input": 284, "tokens_output": 96, "cost_usd": 0.00042, "invocations": 1 },
  "groups": [
    {
      "key": "llama-3.3-70b",
      "tokens_input": 284,
      "tokens_output": 96,
      "cost_usd": 0.00042,
      "invocations": 1
    }
  ]
}

Call the API from your language

Call the API with HTTP (cURL, PHP, TypeScript, or Python), or with the OpenAI SDK. The OpenAI SDK uploads files and creates batches. The Laravel AI SDK and LangChain cannot submit batch jobs; they can still produce the body object you write onto each JSONL line.

Put the base URL and key in configuration once:

export LARAROUTER_API_KEY=lr_sk_…
export LARAROUTER_URL=https://REGION.api.lararouter.com/v1

The body on each JSONL line is an ordinary OpenAI chat, embeddings, or rerank request. Build that object in your language, write the file, then upload and create the batch with the HTTP or OpenAI SDK calls in the steps above.

Where to go next

On this page