Webhooks

Receive a signed HTTPS callback the moment a report or scheduled run finishes, instead of polling. Covers events, signature verification, retries and security.

Webhooks push an event to your server the moment something finishes, so you don’t have to poll GET /v1/jobs/{id} in a loop. You register an HTTPS URL, subscribe it to one or more events, and Rank Prompt POSTs a small signed JSON payload to that URL whenever a matching event fires.

The endpoint CRUD lives under /v1/brands/{brand_id}/webhooks in the API reference. This page explains how delivery, signing and retries actually work.

Events

EventFires when
report.completedA one-off report finished its analysis run.
report.failedA one-off report run failed.
schedule.run.completedA scheduled run finished (every region in the batch).
schedule.run.failedA scheduled run failed.

The payload’s data block is intentionally minimal: it carries the ids and a few headline counts so you can route the event, then you GET the full resource for the detail. The shapes:

// report.completed
{
  "report_id": "8a72d9f1-…",
  "brand_id": "3f9c7e8a-…",
  "scheduled_report_id": null,        // set when the report came from a schedule
  "status": "completed",
  "total_prompts_count": 30,
  "ranked_prompts_count": 17
}

// report.failed (no counts; carries a failure_reason instead)
{
  "report_id": "8a72d9f1-…",
  "brand_id": "3f9c7e8a-…",
  "scheduled_report_id": null,
  "status": "failed",
  "failure_reason": "…"
}

// schedule.run.completed / schedule.run.failed
{
  "scheduled_run_id": "…",            // the batch id; filter reports with ?batch_id=
  "scheduled_report_id": "…",
  "brand_id": "3f9c7e8a-…",
  "status": "completed",              // or "failed"
  "completed_regions_count": 3,
  "total_regions_count": 3,
  "failure_reason": "…"               // present only on a failed run
}

Register an endpoint

Creating an endpoint needs the write:webhooks scope; reading needs read:webhooks. The response includes the signing secret exactly once, store it now.

curl -X POST https://api.rankprompt.com/v1/brands/{brand_id}/webhooks \
  -H "Authorization: Bearer rp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.yourapp.com/hooks/rankprompt",
    "events": ["report.completed", "report.failed"],
    "description": "Prod report pipeline"
  }'

The URL must be a public https:// address. Private, loopback and otherwise non-routable targets are rejected, both at create time and again on every delivery (a DNS-rebinding guard).

The delivery request

Each delivery is a POST with a JSON body and these headers:

HeaderMeaning
X-RP-EventThe event type, e.g. report.completed.
X-RP-Event-IdStable id for the logical event (evt_…). Same across retries; use it to dedupe.
X-RP-Delivery-IdId of this delivery. Stable across the automatic retries of the same delivery (a manual replay creates a new delivery with a new id).
X-RP-SignatureThe HMAC signature (see below).
User-AgentRankPrompt-Webhook/1.0.

The body is the event envelope:

{
  "id": "evt_3f2a…",
  "type": "report.completed",
  "created_at": "2026-06-28T11:42:11.117Z",
  "api_version": "v1",
  "data": { "report_id": "8a72d9f1-…", "brand_id": "3f9c7e8a-…", "status": "completed", "...": "..." }
}

Respond with any 2xx status as soon as you’ve accepted the event. Do the real work asynchronously: only your response status line is read (the body is ignored), and a slow handler eats into the delivery timeout and triggers a retry.

Verify the signature

Every delivery is signed so you can prove it came from Rank Prompt and wasn’t tampered with. The X-RP-Signature header looks like:

t=1719573731,v1=2b8f…hex

v1 is HMAC-SHA256(secret, "{t}.{raw_request_body}"), hex-encoded, where t is the unix timestamp in the same header. To verify:

  1. Split the header into t and the v* signatures (v1, and v0 during a rotation).
  2. Recompute the HMAC over "{t}.{body}" using your endpoint secret and the raw request body (verify before any JSON parsing or re-serialization, which would change the bytes).
  3. Constant-time compare your computed value against each v* value and accept if any matches. During a secret rotation the header carries both v1 (new secret) and v0 (old secret); computing with your current secret matches whichever applies, so verification never breaks mid-rotation.
  4. Reject deliveries whose t is too old (e.g. more than 5 minutes) to blunt replay attacks.
import hashlib
import hmac
import time

def verify(secret: str, signature_header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    ts = parts.get("t")
    if not ts:
        return False
    if abs(time.time() - int(ts)) > tolerance:
        return False  # stale; likely a replay
    expected = hmac.new(secret.encode(), f"{ts}.{raw_body.decode()}".encode(), hashlib.sha256).hexdigest()
    # Accept ANY v* signature: v1 = current secret, v0 = previous secret during the
    # 24h rotation overlap. Whichever your current secret produces is the one that matches.
    return any(
        hmac.compare_digest(expected, val) for key, val in parts.items() if key.startswith("v")
    )

Rotating the secret

POST /v1/brands/{brand_id}/webhooks/{id}/secret-rotations issues a new secret (returned once) and keeps the previous one valid for a 24-hour overlap. During that window deliveries are signed with both: v1 uses the new secret and v0 uses the old one. Accept a delivery if either matches, deploy your new secret, and the old one expires on its own. No downtime.

Retries and failures

A non-2xx response, a timeout, or a connection error is a failed attempt. We retry with a fixed backoff: 30s, 2m, 10m, 1h, 6h, 24h (the initial attempt plus six retries). A delivery that never succeeds ends up dropped.

After 20 consecutive failures the endpoint is auto-disabled: its is_active flips to false, no further events are dispatched to it, and the brand owner gets an in-app and email notification. Fix your handler, then re-enable it:

curl -X PATCH https://api.rankprompt.com/v1/brands/{brand_id}/webhooks/{id} \
  -H "Authorization: Bearer rp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"is_active": true}'

Re-enabling resets the failure counter.

Inspect and replay deliveries

Every attempt is logged. List the recent history for an endpoint to debug:

curl https://api.rankprompt.com/v1/brands/{brand_id}/webhooks/{id}/deliveries \
  -H "Authorization: Bearer rp_live_YOUR_KEY"

Each row carries status, attempt, response_status and error. To force another attempt of a past delivery (e.g. after fixing a bug), replay it:

curl -X POST \
  https://api.rankprompt.com/v1/brands/{brand_id}/webhooks/{id}/deliveries/{delivery_id}/replays \
  -H "Authorization: Bearer rp_live_YOUR_KEY"

Delivery logs are retained for 90 days.

Limits

  • 5 active endpoints per brand; up to 10 events per endpoint.
  • Delivery is rate-limited per endpoint and per brand: bursts past the cap are deferred and delivered shortly after, never dropped.
  • Deliveries for one endpoint are sent in order; different endpoints are delivered in parallel.

What’s next