Webhooks
Instead of polling, register an endpoint and receive a signed POST each time an asynchronous analysis completes.
Register an endpoint
bash
curl -X POST "$API/v1/webhooks?api_key=$ANALYZE_API_KEY" \
-H "Authorization: Bearer $(sign)" -H 'Content-Type: application/json' \
-d '{"url":"https://example.com/hooks/analyze","signal_keys":["is_scammer"]}'ts
const { endpoint, secret } = await sigwise.webhooks.create({
url: "https://example.com/hooks/analyze",
signal_keys: ["is_scammer"],
});python
created = analyze.webhooks.create(url="https://example.com/hooks/analyze", signal_keys=["is_scammer"])
secret = created["secret"]The response includes the endpoint's signing secret, shown only once. Store it with your application's secrets.
signal_keys is optional: with it, the endpoint only receives analyses that answered one of those signals. Update an endpoint with PATCH /v1/webhooks/{endpoint_id} (for example {"enabled": false} to pause it) and remove it with DELETE. Deliveries already queued for a paused endpoint wait and resume when you enable it again.
Choosing events
events picks which event types an endpoint receives. Omit it to receive all of them.
| Event | Sent when |
|---|---|
analysis.completed | Any analysis of an object finishes. |
rule.triggered | A rule whose webhook action points at this endpoint fires. |
To be notified only when a rule matches, for example is_scammer > 50, register an endpoint that receives rule firings and nothing else, then point the rule's webhook action at it:
bash
curl -X POST "$API/v1/webhooks?api_key=$ANALYZE_API_KEY" \
-H "Authorization: Bearer $(sign)" -H 'Content-Type: application/json' \
-d '{"url":"https://example.com/hooks/scammers","events":["rule.triggered"]}'Payload
http
POST /hooks/analyze HTTP/1.1
Content-Type: application/json
User-Agent: analyzeapi-webhooks/1
X-Webhook-Event: analysis.completed
X-Webhook-Delivery: 3f0c1c8e-7a5e-4e0b-9f5d-0a4d7f6c2b11
X-Webhook-Timestamp: 1790000000
X-Webhook-Signature: sha256=5d41402abc4b2a76b9719d911017c592…json
{
"event": "analysis.completed",
"object_id": "user-42",
"tenant_id": "0c6f…",
"model": "model-1",
"occurred_at": "2026-09-27T10:00:05Z",
"answers": [
{ "signal": "is_scammer", "type": "noul", "noul": 0.91 }
]
}answers has the same shape as a direct moderation verdict. Rules with a webhook action send rule.triggered events through the same machinery; both payloads are in the API reference.
Verify every delivery
X-Webhook-Signature is sha256= followed by the hex HMAC-SHA256 of "<X-Webhook-Timestamp>.<raw body>", keyed with the endpoint secret. Verify it against the raw body (before any JSON parsing), compare in constant time, and reject timestamps more than a few minutes old to stop replays.
ts
import { constructWebhookEvent } from "@sigwise/sdk";
app.post("/hooks/analyze", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = constructWebhookEvent(
req.body, // Buffer
req.header("x-webhook-signature"),
req.header("x-webhook-timestamp"),
process.env.ANALYZE_WEBHOOK_SECRET!,
);
handle(event);
res.sendStatus(204);
} catch {
res.sendStatus(400);
}
});python
from sigwise import WebhookVerificationError, construct_webhook_event
@app.post("/hooks/analyze")
def hook():
try:
event = construct_webhook_event(
request.get_data(),
request.headers.get("X-Webhook-Signature"),
request.headers.get("X-Webhook-Timestamp"),
os.environ["ANALYZE_WEBHOOK_SECRET"],
)
except WebhookVerificationError:
return "", 400
handle(event)
return "", 204go
func hook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
ev, err := sigwise.ParseWebhookEvent(body,
r.Header.Get("X-Webhook-Signature"), r.Header.Get("X-Webhook-Timestamp"),
os.Getenv("ANALYZE_WEBHOOK_SECRET"), 0)
if err != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
handle(ev)
w.WriteHeader(http.StatusNoContent)
}php
use SigWise\Webhook;
use SigWise\WebhookVerificationException;
try {
$event = Webhook::constructEvent(
file_get_contents('php://input'),
$_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? null,
$_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? null,
getenv('ANALYZE_WEBHOOK_SECRET'),
);
} catch (WebhookVerificationException $e) {
http_response_code(400);
exit;
}go
func verify(secret, timestamp, signature string, body []byte) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "."))
mac.Write(body)
want := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(want), []byte(signature))
}Retries and the dead-letter queue
Respond with any 2xx quickly (within 10 seconds) and do slow work afterwards.
- Network errors, timeouts,
429and5xxresponses are retried with exponential backoff and jitter (starting at about 30 seconds and doubling), up to 6 attempts. - Other
4xxresponses are permanent and not retried. - A delivery that fails permanently or runs out of attempts moves to the dead-letter queue (
status: dead), keeping its payload and last response.
Deliveries can arrive more than once and out of order. Deduplicate on X-Webhook-Delivery (retries of one delivery share it) and use occurred_at to ignore stale updates.
Inspect deliveries with GET /v1/webhook_deliveries (?status=dead, ?endpoint_id=) and re-send one with POST /v1/webhook_deliveries/{delivery_id}/replay once your endpoint is fixed.