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
| Event | Fires when |
|---|---|
report.completed | A one-off report finished its analysis run. |
report.failed | A one-off report run failed. |
schedule.run.completed | A scheduled run finished (every region in the batch). |
schedule.run.failed | A 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:
| Header | Meaning |
|---|---|
X-RP-Event | The event type, e.g. report.completed. |
X-RP-Event-Id | Stable id for the logical event (evt_…). Same across retries; use it to dedupe. |
X-RP-Delivery-Id | Id 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-Signature | The HMAC signature (see below). |
User-Agent | RankPrompt-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:
- Split the header into
tand thev*signatures (v1, andv0during a rotation). - 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). - Constant-time compare your computed value against each
v*value and accept if any matches. During a secret rotation the header carries bothv1(new secret) andv0(old secret); computing with your current secret matches whichever applies, so verification never breaks mid-rotation. - Reject deliveries whose
tis 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
- Authentication: scopes (
read:webhooks,write:webhooks) and key setup. - Webhooks reference: every endpoint, request and response shape.
- Errors: the canonical error envelope.