Portal
Documentation: all sections

Batch processing

Upload a JSONL file of requests, create a batch, walk away. Sable runs the lines asynchronously within a 24-hour window and hands you back a results file. Because each line runs through the same handlers a live call uses, every line is metered, capped, and gets its own signed receipt — and each one earns a batch discount credited straight back to your balance.

Batch work is the natural fit for evaluation runs, backfills, bulk classification, and embedding a corpus: work with no user waiting on it.

The shape

# 1. Upload the requests
curl -X POST https://api.buildsable.com/v1/files \
  -H "authorization: Bearer $SABLE_KEY" \
  -F purpose=batch \
  -F file=@requests.jsonl

# 2. Create the batch
curl -X POST https://api.buildsable.com/v1/batches \
  -H "authorization: Bearer $SABLE_KEY" \
  -H 'content-type: application/json' \
  -d '{"input_file_id":"file-...","endpoint":"/v1/chat/completions","completion_window":"24h"}'

# 3. Poll, then download
curl https://api.buildsable.com/v1/batches/batch_... \
  -H "authorization: Bearer $SABLE_KEY"

curl https://api.buildsable.com/v1/files/file-.../content \
  -H "authorization: Bearer $SABLE_KEY"

The OpenAI SDKs work unchanged:

f = client.files.create(file=open("requests.jsonl", "rb"), purpose="batch")
b = client.batches.create(
    input_file_id=f.id,
    endpoint="/v1/chat/completions",
    completion_window="24h",
)

The input file

One JSON object per line:

{"custom_id":"row-1","method":"POST","url":"/v1/chat/completions","body":{"model":"sable","messages":[{"role":"user","content":"Classify: great product"}]}}
{"custom_id":"row-2","method":"POST","url":"/v1/chat/completions","body":{"model":"sable","messages":[{"role":"user","content":"Classify: broke on day two"}]}}

Limits: 32 MiB per file, 50,000 requests per file, 20 batches in flight per account. The file is validated line by line at upload, so a malformed file fails while you can still fix it rather than twenty minutes into a run. Errors name a line number and a reason, never the line's content.

What it costs

Each line is billed the ordinary metered price for the work it did, then a batch discount is credited back to your balance as a batch_discount row in the ledger. The deployment's rate is set by the operator; the default is 50%. The discount is idempotent per usage event, so a worker restart can never credit the same line twice.

Two things stay true, because they are what a receipt is worth:

The discount applies to inference and embedding compute. It is not a discount on a different, cheaper backend — it is the same models, the same routing, the same proof, run when it suits us.

Status, cancelling, expiry

status moves validatingin_progressfinalizingcompleted, and request_counts tracks total / completed / failed as it goes.

Subscribe to the batch_completed and batch_failed webhooks instead of polling. Their payloads carry ids, status, and counts only — batch content lives in the sealed output file, never in a webhook body sent to a third-party URL.

The output file

One JSON object per line, in input order:

{
  "id": "batch_req_...",
  "custom_id": "row-1",
  "response": {
    "status_code": 200,
    "request_id": "...",
    "body": { "id": "chatcmpl-...", "choices": [ ... ] },
    "sable_receipt": { "receipt": "...", "signature": "0x...", "signer": "0x..." }
  },
  "error": null
}

Successes land in output_file_id; lines that returned an error land in error_file_id, so a partial failure is easy to retry without filtering.

Files

MethodPathNotes
POST/v1/filesMultipart upload: file and purpose=batch.
GET/v1/filesList your files.
GET/v1/files/:idOne file's metadata.
GET/v1/files/:id/contentDownload the content.
DELETE/v1/files/:idDelete it; the ciphertext is destroyed immediately.

purpose accepts batch only. General-purpose object storage is not something Sable offers, and accepting arbitrary purposes would turn a bounded exception into an unbounded one. Output and error files are produced by the batch worker and cannot be uploaded.

Privacy: the deliberate exception

Everywhere else on Sable, prompts, completions, and submitted code are never persisted. Batching cannot work that way and be honest about it: a worker cannot run a file it does not hold, and you cannot collect results tomorrow that were not kept. So batch files are the disclosed exception, and they are built the way hosted agents and Relay are:

Download your results before the TTL. After it, the metadata remains and the content is gone — including to us. That is the point.

Nothing else changes: individual lines are sealed on ingress and run through the ordinary path, so what reaches the model, and what the receipt records, is identical to a live call.