Overview
When an authenticated API call generates an outcome, Provable.io will POST a signed JSON event to every webhook URL configured on the account. Configure webhooks from the Webhooks tab on your dashboard — each one is created with a signing secret (whsec_…) that is shown once.
Test-mode keys (pk_test_…) intentionally skip webhook delivery so playground traffic does not pollute downstream consumers.
Event types
outcome.created— emitted for every outcome produced by an authenticated call to/api/floats,/api/ints,/api/shuffle,/api/pick,/api/bytes,/api/dice,/api/gaussian,/api/reveal, the batch endpoint, and the streaming endpoint (one event per emitted outcome).test— emitted when you click Send test event in the dashboard. The eventdatais{ "message": "This is a test event from Provable.io" }.
Payload schema
Every event is a JSON object with the same top-level shape:
{
"event": "outcome.created",
"timestamp": "2026-05-24T18:21:07.412Z",
"data": {
"endpoint": "/api/ints",
"outcomeId": "my-app-user-42:0:1",
"clientSeed": "my-app-user-42",
"serverSeedHash": "b4c1…f2",
"nonce": 1,
"cursor": 0,
"values": [37, 8, 91, 42, 14],
"count": 5,
"min": 1,
"max": 100,
"createdAt": "2026-05-24T18:21:07.401Z"
}
}
event— one of the event types above.timestamp— ISO‑8601 UTC timestamp of when the event was generated (when the webhook was queued, not the outcome itself).data— event-specific payload. Foroutcome.created, the fields are:endpoint— the API path that produced the outcome, e.g."/api/ints","/api/shuffle","/api/reveal".outcomeId— stable identifier of the form"{clientSeed}:{cursor}:{nonce}"; useful as a de‑duplication key on your side.clientSeed— the client seed supplied on the originating request.serverSeedHash— the server seed hash you can use to verify the outcome via/api/verifyServerHash.nonce,cursor— the seed-chain position of this outcome.values— the produced outcome (array of numbers, shuffled list, picked items, byte string, etc. — shape depends on the endpoint).count,min,max— request parameters when applicable; may benull/absent for endpoints that don't use them.createdAt— ISO‑8601 timestamp the outcome itself was created on the server.
Note: the webhook event is not a 1:1 copy of the HTTP response body. Fields are renamed for the webhook contract (values instead of the API's outcome, serverSeedHash instead of serverHash), and webhook-only fields like outcomeId and endpoint are added.
Request headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Provable-Webhook/1.0 |
X-Provable-Event | The event type (e.g. outcome.created, test). |
X-Provable-Signature | sha256=<hex> — HMAC‑SHA256 of the raw request body, computed with your webhook's signing secret. |
Verifying the signature
Compute HMAC-SHA256(secret, raw_request_body) as lowercase hex, prefix it with sha256=, and compare to X-Provable-Signature using a constant‑time comparison. Use the raw bytes of the body — re-serializing parsed JSON will change the input and break verification.
Node.js (Express)
const crypto = require("crypto");
const express = require("express");
const app = express();
const SECRET = process.env.PROVABLE_WEBHOOK_SECRET; // whsec_...
// Capture the raw body so the HMAC matches byte-for-byte.
app.post(
"/hooks/provable",
express.raw({ type: "application/json" }),
(req, res) => {
const header = req.get("X-Provable-Signature") || "";
const expected =
"sha256=" +
crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const a = Buffer.from(header);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("bad signature");
}
const event = JSON.parse(req.body.toString("utf8"));
console.log(event.event, event.data);
res.status(200).send("ok");
},
);
app.listen(3000);
Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["PROVABLE_WEBHOOK_SECRET"].encode() # whsec_...
@app.post("/hooks/provable")
def provable_hook():
raw = request.get_data() # raw bytes, before JSON parsing
expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
received = request.headers.get("X-Provable-Signature", "")
if not hmac.compare_digest(expected, received):
abort(401)
event = request.get_json()
print(event["event"], event["data"])
return "ok", 200
curl (compute a signature manually)
Useful for testing your verification code against a fixed body:
BODY='{"event":"test","timestamp":"2026-05-24T18:21:07.412Z","data":{"message":"hi"}}'
SECRET='whsec_replace_me'
SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')"
curl -X POST https://your-app.example.com/hooks/provable \
-H "Content-Type: application/json" \
-H "X-Provable-Event: test" \
-H "X-Provable-Signature: $SIG" \
--data "$BODY"
Retries & backoff
A delivery counts as successful when your endpoint returns an HTTP 2xx status within the timeout. Any other response — including 3xx redirects, which are not followed — is treated as a failure.
- Up to 4 attempts per event (1 initial + 3 retries).
- Exponential backoff between attempts: 500 ms, 1 s, 2 s.
- If all attempts fail, the webhook's Last delivery in the dashboard switches to failed with the last error. The event is not re-queued — design your receiver to be tolerant of occasional missed events and reconcile from the API when needed.
Timeouts
Each HTTP attempt is given 5 seconds before it is aborted and counted as a failed attempt. Acknowledge fast (return 2xx immediately) and do any slow work asynchronously on your side.
Receiver requirements
- Endpoint must be reachable over public
http://orhttps://. Private, loopback, and link-local addresses are rejected at create time and re-checked on every delivery. - Embedded credentials in the URL (
https://user:pass@…) are not allowed. - Redirects are not followed — point the webhook at the final URL.
- Return
2xxas soon as the signature verifies; do business logic after responding.
Next steps
FAQ
When does Provable.io send a webhook?
Whenever a live-mode API key on your account produces an outcome. Each delivery includes the outcome payload, the serverHash, and a short-ID permalink, so your downstream system can persist or react to it without polling.
How do I verify the X-Provable-Signature header?
Compute HMAC-SHA256 of the raw request body using your webhook signing secret, hex-encode it, and constant-time compare to the header value. If it doesn't match, reject the request — the payload didn't come from us (or was tampered with in transit).
How many retry attempts are made?
Failed deliveries are retried with exponential backoff for up to several attempts. The full attempt history is visible in your dashboard under webhook deliveries, with age and per-webhook count retention applied.
What is the per-attempt timeout?
A few seconds per attempt. Your endpoint should respond 2xx quickly and offload heavy processing to a background worker — long requests will be timed out and retried.
Do test-mode (pk_test_*) keys trigger webhooks?
No. Test-mode keys don't touch live seed state and don't fire webhooks, so you can experiment freely without polluting your production delivery history.