Authentication
Every endpoint below is callable anonymously — anonymous calls are free and rate-limited by IP. Authenticated calls are attributed to your account for usage tracking, daily quotas, and webhook delivery.
Send your API key with either of these headers:
x-api-key: YOUR_API_KEYAuthorization: Bearer YOUR_API_KEY
Create or rotate your key on the dashboard.
Live vs. test keys
Every key is either a live key (prefix pk_live_) or a test key (prefix pk_test_). Pick the mode when you create the key on the dashboard.
- Live keys — real cryptographic randomness backed by a server seed you can verify. Calls count toward your daily quota, appear in dashboard usage stats, and trigger any configured webhooks.
- Test keys — outcomes are deterministic: the server derives the seed from
(your account, clientSeed), so the same parameters always return the same numbers (any test key on your account will produce the same outcome for a givenclientSeed). Responses are flagged"mode": "test". Test calls are excluded from live usage stats and quotas and do not fire webhooks, which makes them safe for fixtures, snapshot tests, and CI. They are still subject to the anonymous per-minute rate limit (120 req/min), so they can't be used to bypass abuse protection.
Restricting where a key can be used
Each key (live or test) can carry two optional allowlists, editable from the dashboard:
- Allowed IP ranges — one CIDR per line (IPv4 or IPv6), e.g.
203.0.113.0/24or2001:db8::/32. - Allowed Referer origins — one
scheme://host[:port]per line. A leading*.matches one or more subdomain labels, e.g.https://*.example.com.
Empty list = no restriction. When both lists are set, the request must satisfy both. Requests that fail an active restriction are rejected with HTTP 403 and a JSON body:
{
"error": "Request IP is not in this API key's allowed IP ranges.",
"code": "ip_not_allowed"
}
Possible error codes: ip_not_allowed, referer_not_allowed. Denied requests are logged to the key's activity feed in the dashboard so you can spot leaked or misused keys.
Prefer machines? Download the OpenAPI 3.0 spec (/openapi.json) to generate client SDKs, import into Postman/Insomnia, or run contract tests.
Official SDK
The typed TypeScript client is generated directly from the OpenAPI spec above, so request params and response bodies always stay in sync with the live API.
npm install @provableio/sdk
import { ProvableClient } from "@provableio/sdk";
const client = new ProvableClient({ apiKey: process.env.PROVABLE_KEY });
const { data, error } = await client.getInts({ clientSeed: "order-42", count: 5, min: 1, max: 100 });
if (error) throw new Error(error.error);
console.log(data.outcome);
Zero runtime dependencies; works in Node 18+, browsers, Deno, Bun, and Cloudflare Workers. Source and full docs: provableio/provable-core sdk/typescript · @provableio/sdk on npm.
Try it inline
Every endpoint below has a Run button on its example snippet. Runs are anonymous by default; paste an API key here (or sign in) to attribute them to your account.
Idempotency
Network retries on random-generating endpoints would otherwise re-run the RNG, return a different outcome, and double-count usage. To make retries safe, send an Idempotency-Key header on any call to /api/floats, /api/ints, /api/shuffle, /api/pick, /api/bytes, /api/dice, or /api/gaussian:
curl "https://api.provable.io/api/ints?clientSeed=order-42&count=5" \
-H "x-api-key: $PROVABLE_KEY" \
-H "Idempotency-Key: a3e1c7b9-3a8c-4f1e-9b2d-d8c0e7f4b1aa"
For 24 hours after the first successful call, any retry with the same key returns the original status, body, and Content-Type verbatim. Replays carry an extra response header so callers can tell them apart from fresh draws:
Idempotent-Replayed: true
- Quota-safe. Replays do not increment daily quota, monthly usage, or key activity counters.
- Scope. Keys are scoped to your API key (or to your IP for anonymous calls), so two accounts using the same opaque string never collide.
- Conflicts. Reusing the same key with different query parameters returns
409 Conflictwith body{ "error": "...", "code": "idempotency_key_conflict" }. Pick a new key whenever the request body or params change. - Format. Any opaque ASCII string up to 255 characters — UUIDs (v4) are the typical choice.
- Caching window. Only successful (2xx) responses are stored, for 24 hours. After that the key is forgotten and a retry generates a fresh outcome.
- Where it doesn't apply.
/api/verifyServerHash,/api/listOutcomes, and/api/healthare already side-effect-free, so the header is accepted but ignored on those routes.
Every generation endpoint draws from a hash chain — a server-side row carrying a serverSeed, its public serverHash, and the current cursor/nonce. Each draw bumps the nonce and is recorded into /api/listOutcomes. Chains are addressed by a (api key, clientSeed) pair.
How chains are scoped
- Per-key chains. Every live api key (pk_live_*) has its own private chain for each
clientSeedit uses. Draws made with keyAnever advance keyB's chain, even when both useclientSeed=blackjack-2026. The two are independent hash streams from the moment the chain is created. - Anonymous chain. Calls without an api key share a single anonymous chain per
clientSeed. It's public by design — any anonymous caller can draw from it and any caller can rotate it. Don't use the anon chain for application state you don't want anyone else to influence; create a key. - Test keys.
pk_test_*keys bypass the live chain entirely on every random-generating endpoint (floats,ints,shuffle,pick,bytes,dice,gaussian): outcomes are deterministic per(account, clientSeed), stored as test rows in a per-user fixture space, never advance liveseed_state, and never count toward live usage stats or fire webhooks. Responses are taggedmode: "test".
What spans all chains
Verification and lookup are intentionally global — anyone with a serverHash, a shortId, or a clientSeed can audit any outcome regardless of which chain produced it:
/api/verifyServerHashscans every chain (anon + every key) for the givenclientSeedand returns a match if any chain holds thatserverHash./api/listOutcomes,/api/outcome, and/o/:idpermalinks return outcomes from any chain — the chain identity is internal and not part of the public outcome payload.
Practical guidance
- Each api key + clientSeed pair is a fresh chain. The first draw for a new pair lazily creates the chain; you don't manage it.
- Rotating one chain (see /api/rotate) reveals only that chain's prior
serverSeed— other chains keep theirs secret. - If you need parallel uncorrelated chains under one account (e.g. one per game table), use distinct
clientSeedvalues, the same api key, and rotate them independently.
Generate cryptographically verifiable random floating-point numbers in the range [0, 1).
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
clientSeed | string | Required | Your seed for this generation. Combined with our server seed for provably fair output. |
count | integer (1–100) | Optional | Number of floats to generate. Defaults to 1. |
Example Request
curl "https://api.provable.io/api/floats?clientSeed=my-app&count=5"
const res = await fetch("https://api.provable.io/api/floats?clientSeed=my-app&count=5");
const data = await res.json();
console.log(data.outcome);
import requests
res = requests.get("https://api.provable.io/api/floats?clientSeed=my-app&count=5")
data = res.json()
print(data["outcome"])
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, _ := http.Get("https://api.provable.io/api/floats?clientSeed=my-app&count=5")
defer res.Body.Close()
var data map[string]interface{}
json.NewDecoder(res.Body).Decode(&data)
fmt.Println(data["outcome"])
}
Example Response
{
"outcome": [0.4823, 0.1175, 0.9034, 0.5567, 0.2218],
"clientSeed": "my-app",
"serverHash": "9f86d081884c7d65...",
"nonce": 0,
"cursor": 5,
"count": 5,
"created": 1716566400000,
"shortId": "k7Qm2A9bXz",
"permalink": "https://provable.io/o/k7Qm2A9bXz"
}
The shortId is a stable public ID. Hand anyone the permalink to let them view and re-verify this outcome on a no-auth page. See /o/:id below.
Generate cryptographically verifiable random integers within a custom range.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
clientSeed | string | Required | Your seed for this generation. |
count | integer (1–100) | Optional | Number of integers to generate. Defaults to 1. |
min | integer (≥ 0) | Optional | Minimum value (inclusive). |
max | integer (≥ 1) | Optional | Maximum value (inclusive). |
Example Request
curl "https://api.provable.io/api/ints?clientSeed=my-app&count=5&min=1&max=100"
const res = await fetch("https://api.provable.io/api/ints?clientSeed=my-app&count=5&min=1&max=100");
const data = await res.json();
console.log(data.outcome);
import requests
res = requests.get("https://api.provable.io/api/ints?clientSeed=my-app&count=5&min=1&max=100")
data = res.json()
print(data["outcome"])
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, _ := http.Get("https://api.provable.io/api/ints?clientSeed=my-app&count=5&min=1&max=100")
defer res.Body.Close()
var data map[string]interface{}
json.NewDecoder(res.Body).Decode(&data)
fmt.Println(data["outcome"])
}
Example Response
{
"outcome": [42, 87, 13, 65, 29],
"clientSeed": "my-app",
"serverHash": "9f86d081884c7d65...",
"nonce": 0,
"cursor": 5,
"count": 5,
"min": 1,
"max": 100,
"created": 1716566400000,
"shortId": "k7Qm2A9bXz",
"permalink": "https://provable.io/o/k7Qm2A9bXz"
}
Return a uniformly fair shuffle of a caller-supplied array. The same clientSeed/cursor/nonce always yields the same permutation, so the result is fully verifiable.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
clientSeed | string | Required | Your seed for this generation. |
items | JSON array, comma list, or repeated param | Required | 1–1000 entries to shuffle. Accepts items=["a","b"], items=a,b, or items=a&items=b. |
Example Request
curl "https://api.provable.io/api/shuffle?clientSeed=my-app&items=%5B%22a%22%2C%22b%22%2C%22c%22%2C%22d%22%5D"
const items = ["a", "b", "c", "d"];
const url = `https://api.provable.io/api/shuffle?clientSeed=my-app&items=${encodeURIComponent(JSON.stringify(items))}`;
const res = await fetch(url);
const data = await res.json();
console.log(data.outcome);
import json, requests
items = ["a", "b", "c", "d"]
res = requests.get("https://api.provable.io/api/shuffle", params={"clientSeed": "my-app", "items": json.dumps(items)})
print(res.json()["outcome"])
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
items, _ := json.Marshal([]string{"a", "b", "c", "d"})
q := url.Values{"clientSeed": {"my-app"}, "items": {string(items)}}
res, _ := http.Get("https://api.provable.io/api/shuffle?" + q.Encode())
defer res.Body.Close()
var data map[string]interface{}
json.NewDecoder(res.Body).Decode(&data)
fmt.Println(data["outcome"])
}
Example Response
{
"outcome": ["c", "a", "d", "b"],
"clientSeed": "my-app",
"serverHash": "9f86d081884c7d65...",
"nonce": 0,
"cursor": 0,
"count": 4,
"endpoint": "shuffle",
"created": 1716566400000
}
Pick one item from a list, optionally with per-item weights. Returns the chosen item and its zero-based index.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
clientSeed | string | Required | Your seed for this generation. |
items | JSON array, comma list, or repeated param | Required | 1–1000 entries to choose from. |
weights | JSON array or comma list of numbers | Optional | Non-negative weights, one per item. Defaults to uniform. Must sum to a positive number. |
Example Request
curl "https://api.provable.io/api/pick?clientSeed=loot-roll&items=common,rare,legendary&weights=70,25,5"
const params = new URLSearchParams({
clientSeed: "loot-roll",
items: JSON.stringify(["common", "rare", "legendary"]),
weights: JSON.stringify([70, 25, 5]),
});
const res = await fetch(`https://api.provable.io/api/pick?${params}`);
const data = await res.json();
console.log(data.outcome, data.index);
import json, requests
res = requests.get("https://api.provable.io/api/pick", params={
"clientSeed": "loot-roll",
"items": json.dumps(["common", "rare", "legendary"]),
"weights": json.dumps([70, 25, 5]),
})
data = res.json()
print(data["outcome"], data["index"])
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
items, _ := json.Marshal([]string{"common", "rare", "legendary"})
weights, _ := json.Marshal([]int{70, 25, 5})
q := url.Values{"clientSeed": {"loot-roll"}, "items": {string(items)}, "weights": {string(weights)}}
res, _ := http.Get("https://api.provable.io/api/pick?" + q.Encode())
defer res.Body.Close()
var data map[string]interface{}
json.NewDecoder(res.Body).Decode(&data)
fmt.Println(data["outcome"], data["index"])
}
Example Response
{
"outcome": "legendary",
"index": 2,
"clientSeed": "loot-roll",
"serverHash": "9f86d081884c7d65...",
"nonce": 0,
"cursor": 0,
"count": 1,
"weights": [70, 25, 5],
"endpoint": "pick",
"created": 1716566400000
}
Return count random bytes encoded as hex (default) or base64. Useful for tokens, salts, and key material.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
clientSeed | string | Required | Your seed for this generation. |
count | integer (1–1024) | Optional | Number of bytes to generate. Defaults to 32. |
encoding | hex | base64 | Optional | Output encoding. Defaults to hex. |
Example Request
curl "https://api.provable.io/api/bytes?clientSeed=my-app&count=16&encoding=hex"
const res = await fetch("https://api.provable.io/api/bytes?clientSeed=my-app&count=16&encoding=hex");
const data = await res.json();
console.log(data.outcome);
import requests
res = requests.get("https://api.provable.io/api/bytes?clientSeed=my-app&count=16&encoding=hex")
print(res.json()["outcome"])
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, _ := http.Get("https://api.provable.io/api/bytes?clientSeed=my-app&count=16&encoding=hex")
defer res.Body.Close()
var data map[string]interface{}
json.NewDecoder(res.Body).Decode(&data)
fmt.Println(data["outcome"])
}
Example Response
{
"outcome": "3f0a8b7d1c4e6f209a8c5b4d2e1f0a8b",
"clientSeed": "my-app",
"serverHash": "9f86d081884c7d65...",
"nonce": 0,
"cursor": 0,
"count": 16,
"encoding": "hex",
"endpoint": "bytes",
"created": 1716566400000
}
Roll dice using standard tabletop notation. Returns the individual rolls, the modifier, and the total.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
clientSeed | string | Required | Your seed for this generation. |
notation | string | Required | Dice notation NdM, NdM+K, or NdM-K. N: 1–100 dice. M: 2–1,000,000 sides. Examples: 3d6, 2d20+5, 1d100-10. |
Example Request
curl "https://api.provable.io/api/dice?clientSeed=tabletop¬ation=3d6%2B2"
const res = await fetch("https://api.provable.io/api/dice?clientSeed=tabletop¬ation=" + encodeURIComponent("3d6+2"));
const data = await res.json();
console.log(data.outcome);
import requests
res = requests.get("https://api.provable.io/api/dice", params={"clientSeed": "tabletop", "notation": "3d6+2"})
print(res.json()["outcome"])
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
q := url.Values{"clientSeed": {"tabletop"}, "notation": {"3d6+2"}}
res, _ := http.Get("https://api.provable.io/api/dice?" + q.Encode())
defer res.Body.Close()
var data map[string]interface{}
json.NewDecoder(res.Body).Decode(&data)
fmt.Println(data["outcome"])
}
Example Response
{
"outcome": { "notation": "3d6+2", "rolls": [4, 6, 3], "modifier": 2, "total": 15 },
"clientSeed": "tabletop",
"serverHash": "9f86d081884c7d65...",
"nonce": 0,
"cursor": 0,
"count": 3,
"sides": 6,
"modifier": 2,
"endpoint": "dice",
"created": 1716566400000
}
Draw samples from a normal, exponential, or poisson distribution with caller-supplied parameters.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
clientSeed | string | Required | Your seed for this generation. |
count | integer (1–100) | Optional | Number of samples to draw. Defaults to 1. |
distribution | normal | exponential | poisson | Optional | Defaults to normal. |
mean | number | Optional | μ for normal. Defaults to 0. |
stddev | number (> 0) | Optional | σ for normal. Defaults to 1. |
lambda | number (> 0) | Optional | λ for exponential (> 0) or poisson (> 0, ≤ 100). Defaults to 1. |
Example Request
curl "https://api.provable.io/api/gaussian?clientSeed=stats&distribution=normal&count=5&mean=0&stddev=1"
const res = await fetch("https://api.provable.io/api/gaussian?clientSeed=stats&distribution=normal&count=5&mean=0&stddev=1");
const data = await res.json();
console.log(data.outcome);
import requests
res = requests.get("https://api.provable.io/api/gaussian", params={
"clientSeed": "stats", "distribution": "normal", "count": 5, "mean": 0, "stddev": 1
})
print(res.json()["outcome"])
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, _ := http.Get("https://api.provable.io/api/gaussian?clientSeed=stats&distribution=normal&count=5&mean=0&stddev=1")
defer res.Body.Close()
var data map[string]interface{}
json.NewDecoder(res.Body).Decode(&data)
fmt.Println(data["outcome"])
}
Example Response
{
"outcome": [-0.42, 1.05, 0.18, -1.21, 0.66],
"clientSeed": "stats",
"serverHash": "9f86d081884c7d65...",
"nonce": 0,
"cursor": 0,
"count": 5,
"distribution": "normal",
"mean": 0,
"stddev": 1,
"endpoint": "gaussian",
"created": 1716566400000
}
Open a long-lived Server-Sent Events connection that emits one verifiable outcome per interval, indefinitely (up to a 10 minute cap). Use it for live games, simulators, and dashboards that need a steady drip of randomness without paying the per-request HTTP overhead.
Response type differs from the other endpoints. Instead of a single JSON body the server holds the connection open and writes a stream of text/event-stream frames. Each frame is a self-contained event: outcome message whose data: field is a full JSON outcome (identical shape to the matching non-streaming endpoint, plus an outcomeId field of clientSeed:cursor:nonce). Every emitted outcome verifies independently via /api/verifyServerHash.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
endpoint | string | Required | Which generator to stream: floats, ints, shuffle, pick, bytes, dice, or gaussian. |
clientSeed | string | Required | Your seed for this stream. The same client seed is reused for every emitted outcome. |
intervalMs | integer (100–60000) | Optional | Milliseconds between outcomes. Defaults to 1000. |
| any endpoint params | various | Optional | Pass the same query parameters you would to the underlying endpoint (e.g. count, min, max, items, notation, distribution). Validated once on connect. |
lastEventId | string | Optional | Resume after a disconnect. Pass the most recent outcomeId you received (clientSeed:cursor:nonce); the server will only emit outcomes strictly after that point on the same clientSeed. Equivalent to the Last-Event-ID request header that EventSource sends automatically on auto-reconnect. |
Lifecycle
- The first outcome is emitted immediately after the connection is accepted (it also doubles as parameter validation — bad inputs come back as a normal
400, no stream is opened). - Heartbeat comments (
: heartbeat …) are sent every ~15 seconds so proxies don't time the connection out. - Each emitted outcome counts as one billed call against your account (live keys only). Anonymous and test-mode streams are not metered but still subject to the per-IP anonymous rate limit on the initial connection.
- The server caps any single connection at 10 minutes. When the cap (or quota) is reached the server emits a final
event: doneframe and closes — reconnect to keep streaming. - Every
event: outcomeframe includes anid:line. On a transient drop, vanillaEventSourceauto-reconnects and replays it asLast-Event-ID; rawfetchclients can pass the same value as a?lastEventId=query parameter. Either way the server resumes strictly after the last id you saw on thatclientSeed, so reconnects don't double-count or skip — and skipped duplicates aren't billed. - Closing the connection (e.g.
EventSource.close()) tears down the timers cleanly on the server, so no further outcomes are generated or billed.
Example Request
curl -N "https://api.provable.io/api/stream?endpoint=floats&clientSeed=my-app&intervalMs=1000"
const url = "https://api.provable.io/api/stream?endpoint=floats&clientSeed=my-app&intervalMs=1000";
const es = new EventSource(url);
es.addEventListener("outcome", (e) => {
const o = JSON.parse(e.data);
console.log(o.outcomeId, o.outcome);
});
es.addEventListener("done", (e) => {
console.log("stream closed:", JSON.parse(e.data));
es.close();
});
es.onerror = () => es.close();
Example Stream
id: my-app:0:0
event: outcome
data: {"outcome":[0.4823],"clientSeed":"my-app","serverHash":"9f86d0...","nonce":0,"cursor":0,"count":1,"endpoint":"floats","created":1716566400000,"outcomeId":"my-app:0:0"}
: heartbeat 1716566415000
id: my-app:0:1
event: outcome
data: {"outcome":[0.1175],"clientSeed":"my-app","serverHash":"9f86d0...","nonce":1,"cursor":0,"count":1,"endpoint":"floats","created":1716566401000,"outcomeId":"my-app:0:1"}
event: done
data: {"reason":"max_duration","count":600,"durationMs":600000}
For a full worked example see Streaming outcomes in real time.
Run up to 50 independent draws across any of the random-generating endpoints in a single HTTP round trip. Each draw uses its own clientSeed/cursor/nonce, so every result is independently verifiable via /api/verifyServerHash and has its own shortId/permalink.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
draws | array (1–50) | Required | Array of { endpoint, params } objects. endpoint is one of floats, ints, shuffle, pick, bytes, dice, or gaussian. params takes the same fields you'd pass as query string to the single-draw endpoint (each draw must supply its own clientSeed). |
Behavior
- Per-draw error isolation. A failing draw produces
{ "ok": false, "error": "…" }in its slot — it does not abort the rest of the batch. Successful draws still persist their outcomes. - Quota. Authenticated calls bump usage counters once per successful draw (failures don't count). If the daily quota is hit mid-batch, remaining draws short-circuit with
{ "ok": false, "error": "…", "code": "quota_exceeded" }. - Order.
resultsis in the same order asdraws. - Idempotency. A single
Idempotency-Keyheader covers the whole batch — replays return the identicalresultsarray and do not re-run any draw.
Example Request
curl -X POST "https://api.provable.io/api/batch" \
-H "Content-Type: application/json" \
-d '{
"draws": [
{ "endpoint": "ints", "params": { "clientSeed": "chest-1", "min": 1, "max": 100 } },
{ "endpoint": "ints", "params": { "clientSeed": "chest-2", "min": 1, "max": 100 } },
{ "endpoint": "pick", "params": { "clientSeed": "loot-3", "items": ["common","rare","legendary"], "weights": [70,25,5] } }
]
}'
const res = await fetch("https://api.provable.io/api/batch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
draws: [
{ endpoint: "ints", params: { clientSeed: "chest-1", min: 1, max: 100 } },
{ endpoint: "ints", params: { clientSeed: "chest-2", min: 1, max: 100 } },
{ endpoint: "pick", params: { clientSeed: "loot-3", items: ["common","rare","legendary"], weights: [70,25,5] } },
],
}),
});
const data = await res.json();
for (const r of data.results) console.log(r);
import requests
res = requests.post("https://api.provable.io/api/batch", json={
"draws": [
{"endpoint": "ints", "params": {"clientSeed": "chest-1", "min": 1, "max": 100}},
{"endpoint": "ints", "params": {"clientSeed": "chest-2", "min": 1, "max": 100}},
{"endpoint": "pick", "params": {"clientSeed": "loot-3", "items": ["common","rare","legendary"], "weights": [70,25,5]}},
],
})
for r in res.json()["results"]:
print(r)
Example Response
{
"results": [
{ "ok": true, "outcome": { "outcome": [42], "clientSeed": "chest-1", "serverHash": "9f86d081...", "nonce": 0, "cursor": 0, "count": 1, "min": 1, "max": 100, "endpoint": "ints", "created": 1716566400000, "shortId": "k7Qm2A9bXz", "permalink": "https://provable.io/o/k7Qm2A9bXz" } },
{ "ok": true, "outcome": { "outcome": [73], "clientSeed": "chest-2", "serverHash": "5e3b...", "nonce": 0, "cursor": 0, "count": 1, "min": 1, "max": 100, "endpoint": "ints", "created": 1716566400000, "shortId": "9aB2c3D4eF", "permalink": "https://provable.io/o/9aB2c3D4eF" } },
{ "ok": true, "outcome": { "outcome": "legendary", "index": 2, "clientSeed": "loot-3", "serverHash": "0a1b...", "nonce": 0, "cursor": 0, "count": 1, "weights": [70, 25, 5], "endpoint": "pick", "created": 1716566400000, "shortId": "P9q8R7s6T5", "permalink": "https://provable.io/o/P9q8R7s6T5" } }
]
}
Reveal the current serverSeed for a chain and rotate to a fresh one. This is the only endpoint that exposes the secret serverSeed — every other endpoint surfaces only the public serverHash. After rotation, cursor and nonce reset to 0 on the new serverHash.
Which chain gets rotated
- Authenticated call (pk_live_*) — rotates only that key's chain for the given
clientSeed. Other keys' chains and the anonymous chain are untouched. - Anonymous call (no
x-api-key) — rotates the shared anonymous chain. There's no owner, so any caller can rotate it; treat the revealedserverSeedas public. - Test keys (pk_test_*) — rejected
403(code: "test_mode_forbidden"). Test mode never reveals secrets, and forbidding rotate prevents a test key from exposing the public anon chain'sserverSeed.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
clientSeed | string | Required | The client seed identifying which chain to rotate (combined with the caller's api key, or anon). |
Example Request
curl -X POST https://api.provable.io/api/rotate \
-H "x-api-key: pk_live_..." \
-H "content-type: application/json" \
-d '{"clientSeed":"blackjack-2026"}'
const res = await fetch("https://api.provable.io/api/rotate", {
method: "POST",
headers: {
"x-api-key": "pk_live_...",
"content-type": "application/json",
},
body: JSON.stringify({ clientSeed: "blackjack-2026" }),
});
const { revealed, next } = await res.json();
// verify: sha256(revealed.serverSeed) === revealed.serverHash
import requests
res = requests.post(
"https://api.provable.io/api/rotate",
headers={"x-api-key": "pk_live_..."},
json={"clientSeed": "blackjack-2026"},
)
print(res.json())
package main
import (
"bytes"
"net/http"
)
func main() {
body := bytes.NewBufferString(`{"clientSeed":"blackjack-2026"}`)
req, _ := http.NewRequest("POST", "https://api.provable.io/api/rotate", body)
req.Header.Set("x-api-key", "pk_live_...")
req.Header.Set("content-type", "application/json")
http.DefaultClient.Do(req)
}
Example Response
{
"clientSeed": "blackjack-2026",
"revealed": {
"serverSeed": "b1946ac92492d2347c6235b4d2611184a3a6f3c1e5c1c4d8e6b7f1a2c3d4e5f6",
"serverHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"cursor": 7,
"nonce": 3
},
"next": {
"serverHash": "a3b1c5d7e9f1a3b5c7d9e1f3a5b7c9d1e3f5a7b9c1d3e5f7a9b1c3d5e7f9a1b3",
"cursor": 0,
"nonce": 0,
"rotatedAt": 1716566400000
}
}
Verifying the reveal
To audit fairness, recompute sha256(revealed.serverSeed) and check it matches revealed.serverHash. That hash is what was published with every outcome on this chain since the previous rotate (or since the chain was created), so any /o/:id permalink referencing it can now be re-derived from the revealed seed.
Errors
| Status | Code | When |
|---|---|---|
400 | — | clientSeed missing or empty. |
403 | test_mode_forbidden | Called with a pk_test_* key. |
404 | — | Addressed chain has no recorded outcome yet. For authed calls that's this key's chain for this clientSeed; for anonymous calls it's the anon chain. Generate at least one outcome first. |
Verify that a server hash matches our records for a given client seed. Returns true if the hash is authentic.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
clientSeed | string | Required | The client seed used to generate the outcome. |
serverHash | string | Required | The server hash returned with the original outcome. |
Example Request
curl "https://api.provable.io/api/verifyServerHash?clientSeed=my-app&serverHash=9f86d081884c7d65..."
const res = await fetch("https://api.provable.io/api/verifyServerHash?clientSeed=my-app&serverHash=9f86d081884c7d65...");
const valid = await res.json();
console.log(valid);
import requests
res = requests.get("https://api.provable.io/api/verifyServerHash?clientSeed=my-app&serverHash=9f86d081884c7d65...")
print(res.json())
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, _ := http.Get("https://api.provable.io/api/verifyServerHash?clientSeed=my-app&serverHash=9f86d081884c7d65...")
defer res.Body.Close()
var valid bool
json.NewDecoder(res.Body).Decode(&valid)
fmt.Println(valid)
}
Example Response
{
"verified": true,
"clientSeed": "my-app",
"serverHash": "9f86d081884c7d65...",
"outcomes": [
{
"shortId": "k7Qm2A9bXz",
"permalink": "https://provable.io/o/k7Qm2A9bXz",
"endpoint": "ints",
"cursor": 5,
"nonce": 0,
"created": 1716566400000
}
]
}
When verified, the response also lists permalinks for any recorded outcomes under that (clientSeed, serverHash) pair — convenient for handing a third party a single link.
List all recorded outcomes for a given client seed, in chronological order.
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
clientSeed | string | Required | The client seed to look up. |
Example Request
curl "https://api.provable.io/api/listOutcomes?clientSeed=my-app"
const res = await fetch("https://api.provable.io/api/listOutcomes?clientSeed=my-app");
const outcomes = await res.json();
console.log(outcomes);
import requests
res = requests.get("https://api.provable.io/api/listOutcomes?clientSeed=my-app")
print(res.json())
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, _ := http.Get("https://api.provable.io/api/listOutcomes?clientSeed=my-app")
defer res.Body.Close()
var outcomes []map[string]interface{}
json.NewDecoder(res.Body).Decode(&outcomes)
fmt.Println(outcomes)
}
Example Response
[
{
"outcome": [42, 87, 13, 65, 29],
"clientSeed": "my-app",
"serverHash": "9f86d081884c7d65...",
"nonce": 0,
"cursor": 5,
"created": 1716566400000,
"shortId": "k7Qm2A9bXz",
"permalink": "https://provable.io/o/k7Qm2A9bXz"
}
]
Public, no-auth HTML page for a recorded outcome. Anyone with the shortId can open it to view the endpoint, parameters, client seed, server hash, the generated values, and a server-side Verified ✓ / Mismatch ✗ badge. Includes og:image / og:title so links unfurl in Slack, Discord, and X.
Path Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string (~10 alphanumeric chars) | Required | The shortId returned with the original outcome (also surfaced in /api/listOutcomes and /api/verifyServerHash). |
Example
https://provable.io/o/k7Qm2A9bXz
Unknown IDs return 404. The page also links into /verify so a skeptical reader can independently reproduce the hash.
Stronger fairness guarantee than the one-shot /api/floats and /api/ints. We generate a fresh server seed, publish its serverHash and a commitId, and hold the seed secret. You then submit your client seed to /api/reveal — because the hash was published before you committed your seed, we can't have picked our seed in response to yours. Default TTL is 10 minutes.
Example Request
curl -X POST "https://api.provable.io/api/commit"
const res = await fetch("https://api.provable.io/api/commit", { method: "POST" });
const { commitId, serverHash, expiresAt } = await res.json();
import requests
res = requests.post("https://api.provable.io/api/commit")
print(res.json())
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, _ := http.Post("https://api.provable.io/api/commit", "", nil)
defer res.Body.Close()
var data map[string]interface{}
json.NewDecoder(res.Body).Decode(&data)
fmt.Println(data)
}
Example Response
{
"commitId": "8c3d7b9e-1f2a-4c5d-9e8f-7a6b5c4d3e2f",
"serverHash": "9f86d081884c7d65...",
"expiresAt": 1716567000000
}
Submit your clientSeed and the generation parameters against a commitId from /api/commit. We mark the commit revealed (single-use), run the generation against the committed server seed, persist the outcome, and return the revealed serverSeed for independent verification.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
commitId | string | Required | The commitId returned by /api/commit. |
clientSeed | string | Required | Your seed for this generation. |
endpoint | string | Required | Either "floats" or "ints". |
params | object | Optional | Generator parameters: { count } for floats; { count, min, max } for ints. |
Errors
| Status | When |
|---|---|
404 | The commitId does not exist or was already swept after expiring. |
409 | The commit has already been revealed (single-use). |
410 | The commit expired before being revealed. |
Example Request
curl -X POST "https://api.provable.io/api/reveal" \
-H "Content-Type: application/json" \
-d '{"commitId":"8c3d7b9e-...","clientSeed":"my-app","endpoint":"ints","params":{"count":5,"min":1,"max":100}}'
const commitId = "8c3d7b9e-...";
const res = await fetch("https://api.provable.io/api/reveal", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
commitId, clientSeed: "my-app",
endpoint: "ints", params: { count: 5, min: 1, max: 100 }
})
});
const data = await res.json();
console.log(data);
import requests
commit_id = "8c3d7b9e-..."
res = requests.post("https://api.provable.io/api/reveal", json={
"commitId": commit_id,
"clientSeed": "my-app",
"endpoint": "ints",
"params": {"count": 5, "min": 1, "max": 100}
})
print(res.json())
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]interface{}{
"commitId": "8c3d7b9e-...",
"clientSeed": "my-app",
"endpoint": "ints",
"params": map[string]interface{}{"count": 5, "min": 1, "max": 100},
})
res, _ := http.Post("https://api.provable.io/api/reveal", "application/json", bytes.NewReader(body))
defer res.Body.Close()
var data map[string]interface{}
json.NewDecoder(res.Body).Decode(&data)
fmt.Println(data)
}
Example Response
{
"outcome": [42, 87, 13, 65, 29],
"clientSeed": "my-app",
"serverHash": "9f86d081884c7d65...",
"serverSeed": "b1946ac92492d2347c6235b4d2611184...",
"nonce": 0,
"cursor": 0,
"count": 5,
"min": 1,
"max": 100,
"endpoint": "ints",
"commitId": "8c3d7b9e-1f2a-4c5d-9e8f-7a6b5c4d3e2f",
"created": 1716566400000
}
Service health, version, and uptime. Returns 503 when the database is unreachable.
Example Request
curl "https://api.provable.io/api/health"
const res = await fetch("https://api.provable.io/api/health");
const data = await res.json();
console.log(data);
import requests
res = requests.get("https://api.provable.io/api/health")
print(res.json())
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, _ := http.Get("https://api.provable.io/api/health")
defer res.Body.Close()
var data map[string]interface{}
json.NewDecoder(res.Body).Decode(&data)
fmt.Println(data)
}
Example Response
{
"status": "ok",
"version": "1.0.0",
"uptime": 12345
}
Daily Merkle roots (transparency log)
Every UTC night we publish a SHA-256 Merkle root over every outcome generated that day. Anyone can fetch the root, request an inclusion proof for any outcome, and verify both locally without trusting our database.
The published list lives at /transparency. The scheme is fully deterministic:
- Outcome id:
clientSeed:cursor:nonce - Canonical leaf string:
outcomeId|serverHash|clientSeed|timestampMs - Leaf hash:
SHA256(0x00 || utf8(canonical)) - Internal node:
SHA256(0x01 || left(32) || right(32)) - Odd level: last node duplicated (Bitcoin-style)
- Order: outcomes sorted by
(clientSeed, cursor, nonce)ascending - Empty day: root is
""andleafCount = 0
To verify a proof: start with leaf.hash, then for each sibling combine as SHA256(0x01 || sibling || acc) when position = "left", otherwise SHA256(0x01 || acc || sibling). The final accumulator must equal root.
Get the published Merkle root for a UTC day.
Path Parameters
| Name | Type | Required | Description |
|---|---|---|---|
date | string | Required | UTC date in YYYY-MM-DD form. |
Example Request
curl "https://api.provable.io/api/merkle/2026-05-23"
Example Response
{
"date": "2026-05-23",
"root": "a3f1c2...e7",
"leafCount": 14238,
"treeHeight": 14,
"publishedAt": 1716595500000
}
Get an inclusion proof for a single outcome inside a published daily tree.
Path Parameters
| Name | Type | Required | Description |
|---|---|---|---|
date | string | Required | UTC date in YYYY-MM-DD form. |
outcomeId | string | Required | The outcome's short id, clientSeed:cursor:nonce. |
Example Request
curl "https://api.provable.io/api/merkle/2026-05-23/proof/my-app:0:0"
Example Response
{
"date": "2026-05-23",
"outcomeId": "my-app:0:0",
"leaf": {
"outcomeId": "my-app:0:0",
"serverHash": "9f86d081884c7d65...",
"clientSeed": "my-app",
"timestamp": 1716566400000,
"canonical": "my-app:0:0|9f86d081884c7d65...|my-app|1716566400000",
"hash": "5f4e3d..."
},
"index": 0,
"leafCount": 14238,
"treeHeight": 14,
"root": "a3f1c2...e7",
"publishedAt": 1716595500000,
"siblings": [
{ "position": "right", "hash": "2bcd..." },
{ "position": "left", "hash": "9876..." }
]
}
Paste the response into the verifier widget on /transparency to confirm inclusion client-side.