Batch API

Send an OpenAI Batch JSONL file and get completions from a pinned Hugging Face model, with the OpenAI SDK or plain HTTP.

What the Batch API does

The Batch API is OpenAI-compatible: upload a JSONL file of /v1/chat/completions or /v1/completions requests, create a batch, and download an output file when it ends. The completions come from a public Hugging Face model pinned at an exact commit, generated by the subnet’s miners.

Every sample follows a public forced draw (seeded sampling) that the subnet can check. Outputs are audited before delivery, and tokens that were not verified are not charged.

Batches run on your own file. Batches over catalog environments are not available yet.

Value
Base URLhttps://app.reliqua.ai/api/openai/v1 (https://api.reliqua.ai serves the same routes)
AuthenticationAuthorization: Bearer <API key> with the orders:write scope
ErrorsOpenAI’s shape: {"error": {"message", "type", "param", "code"}}

Authentication — Create an API key with the right scope.

Batch files with an AI agent — The same workflow through the Reliquary MCP tools and official skill.

OpenAI-compatible Batch OpenAPI — Schemas of the /api/openai/v1 file and batch routes.

Routes

Method and path (under /api/openai/v1)Purpose
POST /filesUpload a JSONL file: multipart/form-data with purpose=batch and file (up to 32 MiB).
GET /files, GET /files/{id}, GET /files/{id}/contentList your 100 most recent files, read one, download its bytes.
POST /batchesQuote and order a batch on an uploaded file, in one call.
GET /batches?limit=&after=List your batches, newest first (limit 1–100, default 20).
GET /batches/{id}Read a batch.
POST /batches/{id}/cancelCancel a batch.
GET /files/{output_file_id}/content, GET /files/{error_file_id}/contentDownload the output and error files of a batch.

Write the input file

One JSON object per line, as in OpenAI’s Batch API. custom_id is 1–64 characters and unique in the file; method is POST; url is /v1/chat/completions or /v1/completions; body is the request. A line that cannot be used goes to the error file with its reason; the rest of the file still runs. The whole file is refused only past its limits or when no line is usable (422 invalid_batch_file).

Every line is rendered with the model’s own tokenizer and chat template at the pinned revision. For Qwen3 models, chat_template_kwargs {"enable_thinking": false} turns thinking off.

/v1/chat/completions line

{"custom_id":"q1","method":"POST","url":"/v1/chat/completions","body":{"model":"Qwen/Qwen3-8B","messages":[{"role":"user","content":"Name the capital of France in one word."}],"max_tokens":64,"temperature":0.6,"top_p":0.95,"top_k":20,"seed":7,"chat_template_kwargs":{"enable_thinking":false}}}

/v1/completions line (text or token ids)

{"custom_id":"c1","method":"POST","url":"/v1/completions","body":{"model":"Qwen/Qwen3-8B","prompt":"The capital of France is","max_tokens":64,"temperature":0.6,"top_p":0.95,"top_k":20,"seed":7}}

Accepted and refused body fields

FieldRule
modelOptional on each line; when present it must name the batch’s model (model_mismatch).
messages (chat)Roles system, developer, user, assistant or tool; content is text, a list of text parts, or null. Image parts are refused (multimodal_unavailable).
tools (chat)A list of tool definitions; the model’s chat template must take tools (tools_unavailable).
chat_template_kwargs (chat)Variables the model’s chat template reads, such as enable_thinking (unsupported_template_kwarg otherwise).
prompt (completions)Text, or a list of token ids; one prompt per line.
temperatureAbove 0 and up to 4; default 1. 0 is refused (greedy_unavailable).
top_pAbove 0 and up to 1; default 1.
top_k0 or -1 for no limit (default), or a positive integer.
min_p0 (default) up to, not including, 1.
max_tokens or max_completion_tokens16–131,072 (equal if both are set); default 4,096.
seedOptional integer from 0 to 9,007,199,254,740,991.
nSamples per line, 1–64; default 1. A line’s samples come back as the choices of one output line.
user, metadata, store, parallel_tool_calls, service_tierAccepted and ignored.
stream, logprobs, top_logprobs, frequency_penalty, presence_penalty, repetition_penalty, logit_bias, stop, response_format, tool_choice, echo, suffix, best_ofAccepted only at their neutral value (false, 0, 1, empty, {"type": "text"}, "auto"); anything else is refused, for example stop_unavailable or logprobs_unavailable.
Any other fieldRefused (unsupported_parameter).

One sampling per file

Every usable line of a file must use the same endpoint and the same temperature, top_p, top_k, min_p, max_tokens, seed and n. A file that mixes them is refused with 422 mixed_sampling: split it into one file per sampling. Omitted fields take the defaults above: a line that omits temperature (1) and a line that sets 0.6 use two samplings.

Choose the model and revision

  • The model is a public Hugging Face repository in org/name form. The batch reads it from the first line’s body.model, or from metadata.model when you set it.
  • metadata.revision pins a full 40-character commit. Without it, the commit that main names when you create the batch is pinned. Either way the batch never moves to a newer commit.
  • Supported architectures today: Qwen3ForCausalLM, Qwen3_5ForCausalLM and Qwen3_5ForConditionalGeneration (config.json architectures[0]). GET /api/v1/generation/catalog returns the current list as architectures; another architecture is refused with 422 architecture_unsupported, which names the supported ones.
  • The repository must be public and not gated (model_private), with top-level safetensors weights (model_unsupported).
  • The longest prompt (at least 8,192 tokens are allowed for) plus max_tokens must fit the model’s context length (context_too_long).
  • The weights × 1.3 plus the KV cache of eight sequences at that length must fit one node of up to 8 GPUs (model_too_large). A model size without a published price returns 409 pricing_unavailable.

Models & checkpoints — Pinned revisions and supported models.

Create a batch

POST /batches quotes the file and places the order in one call. The response is the batch object. Send an Idempotency-Key header (16–80 letters, digits, underscores, periods, colons or hyphens) so a retried request returns the same batch instead of creating a second one; the OpenAI SDK retries failed requests automatically, so pass one with extra_headers.

Body fieldValue
input_file_idAn uploaded file id.
endpoint/v1/chat/completions or /v1/completions, the url of the file’s lines (endpoint_mismatch otherwise).
completion_window24h to 168h. When it passes, a running batch stops and what was verified is delivered (status expired).
metadataUp to 16 string pairs (keys up to 64 characters, values up to 512). Returned on the batch.
metadata.revisionOptional full 40-character commit of the model.
metadata.modelOptional org/name; overrides the first line’s body.model.
metadata.max_price_usdOptional price ceiling in USD, as a string. A batch whose reserved price is higher is refused with 402 price_above_max and nothing is reserved.
metadata.reliquary_graderOptional: boxed_math_v1 or exact_match_v1 makes the batch an evaluation (see Evaluations on your file).

Batch states

request_counts.total counts every line of the file. completed counts the lines that have all their samples. failed counts the refused lines and, once the batch has ended, the lines short of samples. The order behind a batch is GET /api/v1/dataset-orders/{order id}, where the order id is the batch id without its batch_ prefix; it shows price_usd, refunded_usd and progress.

statusMeaning
validatingCreated and reserved; waiting to start on the subnet. reliquary_queue says where it is in line (Capacity and queueing).
in_progressMiners are generating.
finalizingGeneration has stopped; the output is being exported (and graded, for an evaluation).
completedDelivered: output_file_id names the output.
expiredThe completion window passed: what was verified by then is delivered; a batch that never started is released in full.
cancellingCancellation requested; the run is stopping.
cancelledCancelled. Before generation starts nothing is charged; later, what was verified is delivered.
failedEnded without a delivery; errors.data[0].code gives the reason.

Read the output and error files

output_file_id is set once the batch is delivered. The output file has one line per input line that received samples, in OpenAI’s batch output format; its n samples are the choices of that line, in sample order, and finish_reason is stop or length.

error_file_id is set when lines were refused or some lines are short of samples. Refused lines come first, with the file line number and the reason; then the lines that did not receive all their samples. A line short of samples can also be in the output with fewer choices.

An output line

{"id":"batch_req_…","custom_id":"q2","response":{"status_code":200,"request_id":"…","body":{"id":"chatcmpl-…","object":"chat.completion","model":"Qwen/Qwen3-8B","created":1762685000,"choices":[{"index":0,"finish_reason":"stop","message":{"role":"assistant","content":"56"},"logprobs":null}],"usage":{"prompt_tokens":24,"completion_tokens":3,"total_tokens":27}}},"error":null}

Error file lines: a refused line, then a line short of samples

{"custom_id":"q3","error":{"code":"stop_unavailable","message":"stop is not offered (only its neutral value is accepted)"},"line":3}
{"custom_id":"q2","prompt_index":1,"error":{"code":"missing_samples","message":"0 of 1 samples delivered"}}

Price, reservation and refund

Creating a batch reserves its price from your balance: the worst case, where every line generates max_tokens tokens for each of its n samples. The price covers the GPU time that verifies the outputs (start-up plus the expected run time), the prompt tokens and the completion tokens at the model’s per-token prices, plus the platform margin; the minimum is 0.01 USD. The GPU share dominates the price of a very small batch: about 2 USD at pilot rates. The quote shows the breakdown in pricing.

At delivery, the completion-token part of the tokens that were not verified is refunded; GPU time and prompt tokens are not. A batch cancelled or failed before generation starts is released in full. Read price_usd and refunded_usd on the order, and your balance on GET /api/v1/balance.

To see the price without reserving anything, quote the uploaded file on the native route (POST /api/v1/dataset-orders/quotes, below): the quote shows price_usd, an estimate assuming half of max_tokens, and pricing details, and expires after 24 hours. metadata.max_price_usd sets a ceiling on POST /batches.

Cancel and list

POST /batches/{id}/cancel before generation starts releases the batch in full: it ends cancelled, at once or after a short cancelling. A running batch becomes cancelling: generation stops, what was verified is delivered and charged, the rest is refunded, and the batch ends cancelled with its output. A finalizing or ended batch returns 409 batch_not_cancellable.

GET /batches lists your batches newest first with first_id, last_id and has_more; pass the last id as after for the next page.

Main error codes

HTTPcodeMeaning
401invalid_api_keyMissing or invalid API key.
403insufficient_scopeThe key does not have orders:write.
403orders_unavailableThe account does not have dataset access yet.
400invalid_endpoint, invalid_completion_window, invalid_metadataA body field is out of range.
400invalid_model, invalid_revisionNo model on the first line or in metadata.model; metadata.revision is not a 40-character commit, or main could not be read.
400endpoint_mismatchendpoint differs from the url of the file’s lines.
404 / 409file_not_found / file_not_readyUnknown file, or an upload still in progress.
413 / 411file_too_large / length_requiredThe multipart upload is over 32 MiB, or has no Content-Length.
422invalid_batch_fileThe whole file is refused: file_too_large, too_many_lines, too_many_tokens or no_usable_line.
422mixed_sampling, max_tokens_conflict, invalid_max_tokensLines use different samplings; the native max_new_tokens differs from the lines’ max_tokens; max_tokens is below 16.
422model_private, model_not_found, model_unsupported, architecture_unsupported, context_too_long, model_too_large, exceeds_generation_deadlineThe model or its size cannot serve this file.
402price_above_max, insufficient_balanceAbove metadata.max_price_usd, or your balance does not cover the price.
429order_limit, rate_limitedFive open orders already, or 200 quotes in the last 24 hours.
409idempotency_conflict, order_refused, batch_not_cancellable, pricing_unavailableSame Idempotency-Key with another body; the order could not be placed (retry); the batch can no longer be cancelled; the model size is not priced.
503batch_unavailable, architecture_list_unavailable, model_lookup_unavailableA dependency is unavailable; retry shortly.

Limits

LimitValue
File size32 MiB on POST /files (multipart); 200 MiB with the native multi-part upload.
Lines per file50,000.
Prompt tokensUp to 131,072 per line and the model’s context; 30,000,000 per file.
Samples per line (n)1–64.
max_tokens16–131,072; 4,096 when no line sets it.
completion_window24h–168h.
Open orders5 at a time, batches and dataset orders together.
Quotes200 per rolling 24 hours (each POST /batches makes one); a native quote expires after 24 hours.
Uploads in progress20 native multi-part uploads at a time.
metadata16 pairs; keys up to 64 characters, values up to 512.

Walkthrough with curl

This script uploads a two-line file, creates a batch on Qwen/Qwen3-8B with 64 tokens per line, polls it every minute and saves output.jsonl. It places a real order: the price, about 2 USD, is reserved from your balance, and metadata.max_price_usd refuses the batch above 3 USD.

batch_curl.sh · places a real order

# Batch API walkthrough with curl: upload a 2-line file, create a batch, poll, download the output.
#
# THIS PLACES A REAL ORDER and charges your Reliquary balance. The quote of even a tiny batch
# includes a fixed share of GPU time (about 2 USD); metadata.max_price_usd below refuses the batch
# if its worst-case price is above 3 USD. Unverified tokens are refunded at delivery.
#
# Needs: curl, jq, and RELIQUARY_API_KEY set to an API key with the orders:write scope.
set -euo pipefail
: "${RELIQUARY_API_KEY:?Set RELIQUARY_API_KEY to an orders:write API key}"

BASE=https://app.reliqua.ai/api/openai/v1
AUTH="Authorization: Bearer $RELIQUARY_API_KEY"
# Qwen/Qwen3-8B at a fixed commit. Omit metadata.revision to pin the repo's current main commit.
REVISION=b968826d9c46dd6066d109eabc6255188de91218
WORK=$(mktemp -d)

# Prints the response; stops the script on an HTTP error (the error body is printed first).
call() {
  local out
  if ! out=$(curl --fail-with-body --silent --show-error "$@"); then
    echo "$out" >&2
    exit 1
  fi
  echo "$out"
}

# 1. The input file: OpenAI Batch JSONL, one request per line, all lines with the same sampling.
cat > "$WORK/batch.jsonl" <<'EOF'
{"custom_id":"q1","method":"POST","url":"/v1/chat/completions","body":{"model":"Qwen/Qwen3-8B","messages":[{"role":"user","content":"Name the capital of France in one word."}],"max_tokens":64,"temperature":0.6,"top_p":0.95,"top_k":20,"seed":7,"chat_template_kwargs":{"enable_thinking":false}}}
{"custom_id":"q2","method":"POST","url":"/v1/chat/completions","body":{"model":"Qwen/Qwen3-8B","messages":[{"role":"user","content":"What is 7 times 8? Reply with the number only."}],"max_tokens":64,"temperature":0.6,"top_p":0.95,"top_k":20,"seed":7,"chat_template_kwargs":{"enable_thinking":false}}}
EOF

# 2. Upload it (multipart, up to 32 MiB on this route).
FILE=$(call -H "$AUTH" -F purpose=batch -F "file=@$WORK/batch.jsonl" "$BASE/files")
echo "$FILE" | jq .
FILE_ID=$(echo "$FILE" | jq -r .id)

# 3. Create the batch: quoted and ordered in one call. The Idempotency-Key makes a retry safe.
BATCH=$(call -H "$AUTH" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: doc-example-$(date +%s)-$RANDOM" \
  --data "$(jq -n --arg file "$FILE_ID" --arg rev "$REVISION" '{
    input_file_id: $file,
    endpoint: "/v1/chat/completions",
    completion_window: "24h",
    metadata: {revision: $rev, max_price_usd: "3"}
  }')" \
  "$BASE/batches")
echo "$BATCH" | jq .
BATCH_ID=$(echo "$BATCH" | jq -r .id)

# 4. Poll until the batch ends.
while :; do
  BATCH=$(call -H "$AUTH" "$BASE/batches/$BATCH_ID")
  STATUS=$(echo "$BATCH" | jq -r .status)
  echo "$(date -u +%H:%M:%S) $STATUS $(echo "$BATCH" | jq -c .request_counts)"
  case "$STATUS" in completed | expired | cancelled | failed) break ;; esac
  sleep 60
done
echo "$BATCH" | jq .

# 5. Download the output and error files when present.
OUTPUT_ID=$(echo "$BATCH" | jq -r '.output_file_id // empty')
ERROR_ID=$(echo "$BATCH" | jq -r '.error_file_id // empty')
if [ -n "$OUTPUT_ID" ]; then
  call -H "$AUTH" "$BASE/files/$OUTPUT_ID/content" > output.jsonl
  echo "output.jsonl:"
  jq -c '{custom_id, text: .response.body.choices[0].message.content, usage: .response.body.usage}' output.jsonl
fi
if [ -n "$ERROR_ID" ]; then
  call -H "$AUTH" "$BASE/files/$ERROR_ID/content" > errors.jsonl
  echo "errors.jsonl:"
  cat errors.jsonl
fi

The same with the OpenAI Python SDK

Point the official openai package at the Batch API base URL with your Reliquary key. The SDK’s type hints list only 24h for completion_window and OpenAI’s endpoints; the values above are accepted. This script places a real order too.

batch_openai.py · places a real order

"""Batch API walkthrough with the official openai SDK (pip install openai).

THIS PLACES A REAL ORDER and charges your Reliquary balance. The quote of even a tiny batch
includes a fixed share of GPU time (about 2 USD); metadata.max_price_usd below refuses the batch
if its worst-case price is above 3 USD. Unverified tokens are refunded at delivery.

Needs RELIQUARY_API_KEY set to an API key with the orders:write scope.
"""
import json
import os
import pathlib
import sys
import tempfile
import time
import uuid

import openai

client = openai.OpenAI(
    base_url="https://app.reliqua.ai/api/openai/v1",
    api_key=os.environ["RELIQUARY_API_KEY"],
)
# Qwen/Qwen3-8B at a fixed commit. Omit metadata.revision to pin the repo's current main commit.
REVISION = "b968826d9c46dd6066d109eabc6255188de91218"


def line(custom_id: str, question: str) -> dict:
    return {
        "custom_id": custom_id,
        "method": "POST",
        "url": "/v1/chat/completions",
        "body": {
            "model": "Qwen/Qwen3-8B",
            "messages": [{"role": "user", "content": question}],
            "max_tokens": 64,
            "temperature": 0.6,
            "top_p": 0.95,
            "top_k": 20,
            "seed": 7,
            "chat_template_kwargs": {"enable_thinking": False},
        },
    }


try:
    # 1. The input file: one request per line, all lines with the same sampling.
    path = pathlib.Path(tempfile.mkdtemp()) / "batch.jsonl"
    path.write_text(
        "".join(
            json.dumps(row) + "\n"
            for row in [
                line("q1", "Name the capital of France in one word."),
                line("q2", "What is 7 times 8? Reply with the number only."),
            ]
        )
    )

    # 2. Upload it.
    with path.open("rb") as handle:
        file = client.files.create(file=handle, purpose="batch")
    print(file.model_dump_json(indent=2))

    # 3. Create the batch. The Idempotency-Key makes the SDK's automatic retries safe.
    batch = client.batches.create(
        input_file_id=file.id,
        endpoint="/v1/chat/completions",
        completion_window="24h",
        metadata={"revision": REVISION, "max_price_usd": "3"},
        extra_headers={"Idempotency-Key": "doc-example-" + uuid.uuid4().hex},
    )
    print(batch.model_dump_json(indent=2))

    # 4. Poll until the batch ends.
    while batch.status not in ("completed", "expired", "cancelled", "failed"):
        time.sleep(60)
        batch = client.batches.retrieve(batch.id)
        print(time.strftime("%H:%M:%S"), batch.status, batch.request_counts)
    print(batch.model_dump_json(indent=2))

    # 5. Download the output and error files when present.
    if batch.output_file_id:
        output = client.files.content(batch.output_file_id).text
        for row in map(json.loads, output.splitlines()):
            body = row["response"]["body"]
            print(row["custom_id"], repr(body["choices"][0]["message"]["content"]), body["usage"])
    if batch.error_file_id:
        print(client.files.content(batch.error_file_id).text)
except openai.APIStatusError as error:
    print(error.status_code, error.response.text, file=sys.stderr)
    sys.exit(1)

The native route

The same order is available on the native API with the same orders:write key. Use it for files over 32 MiB, to see a quote before ordering, or for a Parquet delivery.

StepRoute
Start an uploadPOST /api/v1/files {"filename", "bytes" (up to 209,715,200), "purpose": "batch"}: the answer gives upload.part_size (8 MiB) and upload.parts.
Send each partPUT /api/v1/files/{id}/parts/{n} with the raw bytes (n from 1); optional X-Part-Sha256. Every part but the last is exactly part_size bytes; a part can be retried.
Complete itPOST /api/v1/files/{id}/complete.
QuotePOST /api/v1/dataset-orders/quotes {"model", "revision" (40 characters, required), "input_file_id", "max_new_tokens"? (for lines without max_tokens), "output_format"? (parquet by default, or openai_jsonl), "grader"?}. Nothing is reserved; the answer includes line_errors and a preview of the rendered prompts.
OrderPOST /api/v1/dataset-orders {"quote_id"} with an Idempotency-Key header (16–80 letters, digits, underscores or hyphens).
Follow and downloadGET /api/v1/dataset-orders/{id}; its deliveries list the files of delivery dl-{id} (output-*.jsonl or part-*.parquet, errors-*.jsonl, report.json, manifest.json), each with a download url.

Dataset orders API — Order states, cancellation and downloads.

Generation OpenAPI — Schemas of the native file and quote routes.