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

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"
    }
}

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

HeaderValue
Content-Typeapplication/json
User-AgentProvable-Webhook/1.0
X-Provable-EventThe event type (e.g. outcome.created, test).
X-Provable-Signaturesha256=<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.

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

Next steps