Webhooks
Signed, at-least-once delivery of durable state changes, with a stable delivery id across retries and replays.
One endpoint per project, configured on its Connect page. Webhooks report durable state changes.
{
"schema_version": "2026-08-01",
"event_id": "event_f08c",
"type": "prospect.outreach.approved",
"created_at": "2026-08-15T10:22:18Z",
"project_id": "project_128",
"data": { ... }
}
data is the canonical prospect result for a result event, the stage-job representation for a requested, rejected, failed or expired stage event, and a cycle representation for cycle.completed.
Event types
| Type | Meaning |
|---|---|
| prospect.qualification.requested | External qualification job available |
| prospect.research.requested | External research job available |
| prospect.outreach.requested | External outreach job available |
| prospect.stage_result.accepted | External result accepted |
| prospect.stage_result.rejected | External result needs correction |
| prospect.stage.failed | External worker reported failure |
| prospect.stage.expired | Stage job reached its deadline |
| prospect.research.ready | Research supports an angle |
| prospect.research.needs_review | Research needs review |
| prospect.research.not_suitable | Research found no defensible angle |
| prospect.outreach.ready | Outreach bundle ready for approval |
| prospect.outreach.approved | Outreach bundle approved |
| prospect.stale | Current work became stale |
| prospect.failed | Prospect reached a terminal failure |
| cycle.completed | Cycle finished |
Verifying a signature
Two headers. Prospex-Signature: t=<unix>,v1=<hex> is HMAC-SHA256 over "<t>." + body, using the exact bytes we sent, so read the raw body before any JSON parsing or re-encoding. Prospex-Delivery is your idempotency key.
import hashlib, hmac, time
def verify(header, body, secret, tolerance=300):
parts = dict(p.strip().split("=", 1) for p in header.split(","))
timestamp = int(parts["t"])
if abs(time.time() - timestamp) > tolerance:
return False
expected = hmac.new(
secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
).hexdigest()
# There may be more than one v1 during a rotation. Check them all.
return any(
hmac.compare_digest(expected, value)
for key, value in (p.strip().split("=", 1) for p in header.split(","))
if key == "v1"
)
Rotating a secret publishes both for 24 hours: the header carries a v1 for each, newest first. Loop over them.
Delivery
- Any
2xxcompletes a delivery. Timeouts,429and5xxretry with bounded backoff. A permanent4xxfails without looping. - Delivery is at least once, and events from different prospects can arrive out of order. Use the event time and revision numbers to decide what is newest.
- The delivery ID is stable across a retry and a manual replay of the same event. Store processed IDs.
- A retry carries the payload captured when the event happened, not the current state. Read the prospect through the API when you want what is true now.
- One event cannot happen twice inside Prospex.
- After 20 consecutive terminal failures the endpoint is paused and its owner emailed once. The project keeps running, and anything that happens while it is off is held rather than dropped. Saving the endpoint again releases the held deliveries in order.
- Events stay replayable for 30 days.
- A requested-stage event is sent only once the job exists. The claim on the job decides which worker may submit a result.