Plans and ownership
| Plan | Endpoints | Owner |
|---|---|---|
| Free | 0 | May remove a revoked legacy tombstone after downgrade |
| Pro | 3 | Individual user |
| Team | Unlimited by plan | Organization |
| Enterprise | Unlimited by plan | Organization |
“Unlimited by plan” means there is no lower commercial endpoint-count limit. Fair-use security controls allow up to 1,000 active or paused endpoints per owner, up to three destinations per hostname, 10 create or secret-rotation requests per user per minute, and 100 endpoint-creation attempts per owner in a rolling 24-hour window. Rejected creation attempts count toward the daily owner limit; secret rotations do not. Organization owners and admins manage Team and Enterprise endpoints.
Configure and test
- Open Webhooks in the authenticated console and add a public HTTPS destination on port 443. The hostname must have a public IPv4 address; literal IPs, redirects, fragments, embedded credentials, and any non-public DNS answer are rejected.
- Copy the signing secret from the one-time modal into your secret manager. It is never returned by list/detail requests and cannot be recovered later.
- Select Test.
POST /v1/webhooks/{id}/testreturns202with a pending delivery id; it does not perform inline egress. - Poll the endpoint’s delivery history until that id is succeeded or terminal. A
2xxfrom your receiver means success.
{
"delivery_id": 981,
"event_type": "webhook.test",
"status": "pending"
}
Headers and signed bytes
Every attempt carries these headers:
X-RealExploit-Timestamp: <Unix seconds>
X-RealExploit-Signature: v1=<lowercase HMAC-SHA256 hex>
X-RealExploit-Delivery-ID: <stable delivery id>
X-RealExploit-Event: verdict.changed | webhook.test
X-RealExploit-Event-Id: <stable event id>
X-RealExploit-Schema-Version: 1
The exact HMAC message is:
<timestamp>.<delivery_id>.<raw HTTP body bytes>
Verify against the untouched request body before parsing JSON. Do not parse and reserialize it: even equivalent JSON can produce different bytes. Reject timestamps outside your replay window and compare the signature in constant time.
Python verification
import hashlib
import hmac
import time
def verify_webhook(secret: str, headers: dict[str, str], raw_body: bytes) -> bool:
timestamp_text = headers.get("x-realexploit-timestamp", "")
delivery_id = headers.get("x-realexploit-delivery-id", "")
received = headers.get("x-realexploit-signature", "")
if not isinstance(raw_body, bytes):
return False
if not timestamp_text.isascii() or not timestamp_text.isdigit():
return False
if not delivery_id.isascii() or not delivery_id.isdigit() or int(delivery_id) <= 0:
return False
timestamp = int(timestamp_text)
if abs(int(time.time()) - timestamp) > 300:
return False
message = f"{timestamp_text}.{delivery_id}.".encode("utf-8") + raw_body
digest = hmac.new(secret.encode("utf-8"), message, hashlib.sha256).hexdigest()
signature_ok = hmac.compare_digest(f"v1={digest}", received)
return signature_ok and headers.get("x-realexploit-schema-version") == "1"
Node.js verification
Your HTTP framework must give this function the original Buffer, not a parsed object.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(secret, headers, rawBody) {
const timestamp = String(headers["x-realexploit-timestamp"] ?? "");
const deliveryId = String(headers["x-realexploit-delivery-id"] ?? "");
const receivedText = String(headers["x-realexploit-signature"] ?? "");
if (!Buffer.isBuffer(rawBody)) return false;
if (!/^\d+$/.test(timestamp)) return false;
if (!/^[1-9]\d*$/.test(deliveryId)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) return false;
const message = Buffer.concat([
Buffer.from(`${timestamp}.${deliveryId}.`, "utf8"),
rawBody,
]);
const expected = Buffer.from(
`v1=${createHmac("sha256", secret).update(message).digest("hex")}`,
"utf8",
);
const received = Buffer.from(receivedText, "utf8");
const signatureOk = expected.length === received.length && timingSafeEqual(expected, received);
return signatureOk && String(headers["x-realexploit-schema-version"] ?? "") === "1";
}
Replay safety and idempotency
- Use a short replay window; five minutes is a reasonable default.
- Persist
X-RealExploit-Delivery-IDin a uniqueness-constrained table before applying side effects. - Compare
occurred_atandevent_idwith your last applied state; concurrent deliveries for one endpoint are not ordered. - Return
2xxonly after durable acceptance. If your process accepts a request and crashes before responding, RealExploit may deliver it again. - Treat event and schema headers as untrusted until the signature passes. Then parse the signed body and require its type/version to match the supported headers.
- Keep webhook processing independent from user-facing request latency; enqueue locally when possible.
Event body
{
"data": {
"cve_id": "CVE-2021-44228",
"previous_verdict": "POC_AVAILABLE",
"score": 95,
"score_version": 1,
"verdict": "ACTIVELY_EXPLOITED"
},
"event_id": 123,
"occurred_at": "2026-08-10T02:00:00Z",
"schema_version": 1,
"type": "verdict.changed"
}
Bodies are compact, sorted-key UTF-8 JSON. Delivery history exposes the public CVE transition and bounded delivery status only; RealExploit does not persist response bodies or arbitrary remote error text.
Version 1 has no per-CVE subscription filter. Each eligible active
endpoint receives every globally captured verdict.changed
event plus its own manual tests. Dimension your receiver accordingly
and filter only after verifying the signature.
Retries and terminal results
RealExploit makes up to nine attempts: initial delivery, then roughly
1 minute, 5 minutes, 25 minutes, 2 hours, 6 hours, 12 hours, 18 hours,
and 24 hours later. Retry-After is honored for
429/503 within a bounded 1-minute to 6-hour
window plus jitter, but never beyond the absolute 24-hour retry-cycle
deadline. An authorized manual retry starts a new cycle without
deleting the prior attempt audit.
2xx: succeeded.408,409,425,429, and5xx: retryable.- Redirects, other
4xx, invalid TLS, and unsafe destinations: permanent failure. - After the final unsuccessful attempt, the delivery is
dead. Rotate/test or correct the endpoint; do not assume a manual retry will bypass eligibility checks. - Pause, delete, secret rotation, destination replacement, or entitlement loss cancels runnable deliveries for the old configuration. They remain
cancelledin history and are not replayed after resume or recreation.
Ready to integrate?
Check current plan pricing, then use the authenticated console when Webhooks is visible for your environment.