prospex
Prospex docs

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

TypeMeaning
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 2xx completes a delivery. Timeouts, 429 and 5xx retry with bounded backoff. A permanent 4xx fails 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.

Cookie preferences

Necessary cookies always run. The other two are on unless you turn them off.