Webhooks let Tempered notify your systems when evaluations complete, removing the need to poll for results.
evaluation.completed, evaluation.failed)curl -X POST https://your-tempered-instance/api/v1/webhooks/ \
-H "Authorization: Bearer prx_your_admin_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/praxis",
"events": ["evaluation.completed", "evaluation.failed"]
}'
Response:
{
"id": "wh-uuid-here",
"url": "https://your-app.example.com/webhooks/praxis",
"secret": "whsec_your_signing_secret",
"events": ["evaluation.completed", "evaluation.failed"],
"is_active": true
}
| Event | Trigger |
|---|---|
evaluation.completed |
An evaluation finished successfully with a verdict |
evaluation.failed |
An evaluation failed (timeout, all vendors failed, etc.) |
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"description": "Deploy updated auth service...",
"context": {"environment": "production"},
"change_request_id": "CHG-1234",
"plan_id": "PLN-456",
"recommendation": "PROCEED_WITH_MITIGATIONS",
"approved": true,
"opinions": [
{
"vendor_name": "anthropic",
"model_id": "claude-sonnet-4-6",
"recommendation": "PROCEED_WITH_MITIGATIONS",
"confidence": 0.85,
"dimensions": {
"security": {"level": "low", "rationale": "..."},
"reliability": {"level": "medium", "rationale": "..."}
},
"conditions": ["Verify staging test results before deployment"]
}
]
}
Every webhook request includes an HMAC-SHA256 signature for verification. Always verify signatures to ensure the payload came from Tempered.
| Header | Content |
|---|---|
X-Praxis-Signature |
HMAC-SHA256 signature |
X-Praxis-Timestamp |
Unix timestamp of the request |
X-Praxis-Event |
Event type (e.g., evaluation.completed) |
The signature is computed as:
HMAC-SHA256(secret, "{timestamp}.{payload_json}")
import hashlib
import hmac
import time
def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
# Reject stale timestamps (>5 minutes old)
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.{payload.decode()}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_signing_secret"
@app.route("/webhooks/praxis", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Praxis-Signature", "")
timestamp = request.headers.get("X-Praxis-Timestamp", "")
if not verify_webhook(request.data, signature, timestamp, WEBHOOK_SECRET):
return jsonify({"error": "Invalid signature"}), 401
data = request.json
event = request.headers.get("X-Praxis-Event")
if event == "evaluation.completed":
handle_evaluation_result(data)
return jsonify({"status": "ok"}), 200
If your endpoint returns a non-2xx status code or times out, Tempered retries:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 2 minutes |
| 3rd retry | 10 minutes |
After 3 failed attempts, the delivery is moved to a dead letter queue (DLQ). Failed deliveries can be reviewed and retried from the webhook management page.
id for idempotent processing