When to batch vs. loop
Looping GET /api/ints N times costs N round-trips and N rate-limit slots. POST /api/batch takes a single round trip and counts as one rate-limit slot for the batch dispatch (each successful draw still bumps quota individually). Reach for batch when:
- You have a handful of independent draws that all need to land before you can move on (rolling loot for every chest in a treasure room; assigning starting hands at the top of a round; bucketing a list of users into A/B variants).
- You'd otherwise be tempted to fire
Promise.all([...])across the API.
Don't batch when:
- You just need many values from one generator with one seed — use
count=on the underlying endpoint and one call. - The draws are sequential — the second one depends on the first's outcome. Batch runs draws independently and doesn't pipe results between them.
Request shape
The body is an object with a draws array. Each draw is { endpoint, params }; endpoint is the bare generator name (floats, ints, shuffle, pick, bytes, dice, gaussian) and params is exactly the query string that endpoint would normally take. Up to 50 entries per batch.
curl -X POST https://api.provable.io/api/batch \
-H "Content-Type: application/json" \
-d '{
"draws": [
{ "endpoint": "ints", "params": { "clientSeed": "round-1:chest-1", "count": 1, "min": 1, "max": 100 } },
{ "endpoint": "ints", "params": { "clientSeed": "round-1:chest-2", "count": 1, "min": 1, "max": 100 } },
{ "endpoint": "pick", "params": { "clientSeed": "round-1:loot-3", "items": ["common","rare","legendary"], "weights": [70,25,5] } },
{ "endpoint": "dice", "params": { "clientSeed": "round-1:boss", "notation": "2d20+5" } }
]
}'
Response shape & ordering
You always get back a results array of length N, in the same order as the request. Each element is either { ok: true, outcome } with the full outcome shape that the matching standalone endpoint would have returned (shortId, permalink, serverHash, etc.) or { ok: false, error, code? }.
{
"results": [
{ "ok": true, "outcome": { "outcome": [42], "shortId": "k7Qm2A9bXz", "permalink": "https://provable.io/o/k7Qm2A9bXz", "serverHash": "9f…", "clientSeed": "round-1:chest-1", "cursor": 0, "nonce": 0, "endpoint": "ints", "min": 1, "max": 100, "count": 1, "created": 1716566400000 } },
{ "ok": true, "outcome": { "outcome": [73], "shortId": "9aB2c3D4eF", "permalink": "https://provable.io/o/9aB2c3D4eF", "serverHash": "5e…", "clientSeed": "round-1:chest-2", "cursor": 0, "nonce": 0, "endpoint": "ints", "min": 1, "max": 100, "count": 1, "created": 1716566400000 } },
{ "ok": false, "error": "items required" },
{ "ok": true, "outcome": { "outcome": { "notation": "2d20+5", "rolls": [14, 9], "modifier": 5, "total": 28 }, "shortId": "P9q8R7s6T5", "permalink": "https://provable.io/o/P9q8R7s6T5", "serverHash": "0a…", "clientSeed": "round-1:boss", "cursor": 0, "nonce": 0, "endpoint": "dice", "count": 2, "sides": 20, "modifier": 5, "created": 1716566400000 } }
]
}
Partial failure handling
A single bad draw does not abort the batch — it just shows up as ok: false in that slot. The successful draws still persist their outcomes, fire webhooks (live keys), and count against quota. Your client code should iterate the array and handle each ok: false the same way it would handle a failed individual call:
const { results } = await batch(req);
const okOutcomes = [];
const errors = [];
results.forEach((r, i) => {
if (r.ok) okOutcomes.push({ index: i, outcome: r.outcome });
else errors.push({ index: i, draw: req.draws[i], error: r.error, code: r.code });
});
if (errors.length) console.warn("batch had partial failures:", errors);
Two specific failure codes are worth handling:
code: "quota_exceeded". Returned once the daily quota is hit. Every remaining draw in the batch short-circuits took: falsewith this code instead of running.code: "idempotency_replay". If you reuse anIdempotency-Keywith a different batch body, you'll get a top-level409 Conflicton the whole batch.
Attributing a whole batch to a single round
Two patterns work well, depending on what you need to prove later:
Pattern A — encode the round into every clientSeed
Prefix every draw's clientSeed with the round id (e.g. "round-1:chest-1", "round-1:chest-2"). Now GET /api/listOutcomes?clientSeed=round-1:chest-1 tells you exactly which seed produced each result, and players can look up any draw's permalink without seeing the others.
Pattern B — one Idempotency-Key per round
Generate one key per round, send it as the Idempotency-Key header on the batch request, and store it alongside the round in your DB. A retry replays the exact same results — no draw runs twice, no double-billing. See Using Idempotency-Key for the full retry pattern.
Pattern A makes individual draws auditable; Pattern B makes the whole round retryable. Use both together for high-stakes rounds.
Limits to be aware of
- 50 draws max per batch. Bigger workloads should chunk and fan out.
- Each successful draw counts once against quota. A 30-draw batch is 30 calls against your
DAILY_QUOTA. - Webhooks fire per successful draw, in unspecified order — your receiver should already be tolerant of that.
- Test-mode batches work fine: every draw inside reuses the same deterministic, quota-free behavior described in Test mode vs live mode.