Lararouter
API reference

Embeddings

Embedding request bodies on a batch line. There is no live POST /v1/embeddings.

Not a live HTTP endpoint

Do not POST to /v1/embeddings. Put that path on each JSONL line as url, then submit the file with POST /v1/batches and "endpoint": "/v1/embeddings".

OpenAI-compatible embeddings, as the body of a batch line. Embedding work is almost always a backfill or an indexing job, so it was already the clearest fit for running as a job: results can land on a schedule, and pricing sits below OpenRouter batch inference.

Requires the batches:write scope on the key that creates the batch.

requests.jsonl
{"custom_id":"doc_5501","method":"POST","url":"/v1/embeddings","body":{"model":"bge-large-en-v1.5","input":["Refunds are issued within 5 business days.","Shipping is free over $50."]},"metadata":{"entity":"customer:cus_2291"}}

Submit the file:

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/embeddings",
  "completion_window": "24h"
}

Every line in a file must target the same endpoint, so chat and embedding work go in separate batches.

Parameters

Prop

Type

Response

Each result line pairs your custom_id with the embeddings response for that input array.

{
  "custom_id": "doc_5501",
  "response": {
    "status_code": 200,
    "request_id": "req_01JD8XK4M2",
    "body": {
      "object": "list",
      "model": "bge-large-en-v1.5",
      "data": [
        { "object": "embedding", "index": 0, "embedding": [0.0023, -0.0091, 0.0142] },
        { "object": "embedding", "index": 1, "embedding": [0.0117, 0.0038, -0.0064] }
      ],
      "usage": { "prompt_tokens": 24, "total_tokens": 24, "cost_usd": 0.0000012 }
    }
  },
  "error": null
}

Match on index, not order

data is returned in request order today, but relying on that is fragile. Every item carries its index. Use it to line vectors up with the inputs on that line, and custom_id to line the line up with your own records.

Packing lines

input takes an array, so a line can carry many documents. Fewer, fatter lines mean a smaller file and fewer audit log entries: 500 documents across two lines rather than 500 lines.

Pack to the token limit rather than the item limit: 2,048 short strings fit easily, 2,048 long documents will not. Chunks of 128–256 are a reasonable default.

namespace App\Actions\Lararouter;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Throwable;

class EmbedDocuments
{
    public function handle(Collection $documents, int $chunkSize = 256): string
    {
        try {
            $path = Storage::path('batches/embeddings-'.now()->timestamp.'.jsonl');
            $handle = fopen($path, 'w');

            $documents->chunk($chunkSize)->each(function (Collection $chunk) use ($handle) {
                fwrite($handle, json_encode([
                    'custom_id' => 'chunk_'.md5($chunk->pluck('id')->implode('-')),
                    'method' => 'POST',
                    'url' => '/v1/embeddings',
                    'body' => [
                        'model' => 'bge-large-en-v1.5',
                        'input' => $chunk->pluck('body')->values()->all(),
                    ],
                ]).PHP_EOL);
            });

            fclose($handle);

            $file = Http::withToken(config('services.lararouter.key'))
                ->attach('file', file_get_contents($path), basename($path))
                ->post('https://REGION.api.lararouter.com/v1/files', ['purpose' => 'batch'])
                ->throw()
                ->json();

            return Http::withToken(config('services.lararouter.key'))
                ->withHeaders(['Idempotency-Key' => 'embed-'.$file['id']])
                ->post('https://REGION.api.lararouter.com/v1/batches', [
                    'input_file_id' => $file['id'],
                    'endpoint' => '/v1/embeddings',
                    'completion_window' => '24h',
                ])
                ->throw()
                ->json('id');
        } catch (Throwable $e) {
            report($e);

            throw $e;
        }
    }
}

Deriving custom_id from the chunk contents means the mapping back to documents survives a retry, and the idempotency key on the create stops a re-run paying to embed the same documents twice. See Idempotency.

Cost

Embeddings bill on input tokens only. There is no output charge. Grouping usage by model separates embedding spend from chat spend:

curl -G https://REGION.api.lararouter.com/v1/usage/entities \
  -H "Authorization: Bearer $LARAROUTER_API_KEY" \
  -d "start=2026-08-01" \
  -d "group_by=model"

On this page