VisoraDocs
APIDashboard
VISORA WEBHOOKS DOCS

React to moderation and redaction events

Webhooks push signed events to your backend when a moderation decision is made, a review is resolved, or a redaction finishes. Deliveries are asynchronous, so they never slow your API calls.

Create endpoints per project from Dashboard → Webhooks and subscribe to the events you care about.

i

Every delivery is HMAC-signed. Always verify the signature before trusting an event — the SDK helpers do this for you.

Event types

Subscribe an endpoint to one or more events. Moderation projects emit moderation and review events; redaction projects emit redaction.completed.

EventFired when
moderation.completedEvery successful POST /moderate decision.
moderation.review_requiredA moderation decision was queued for manual review.
review.approvedA queued review was approved in the dashboard.
review.rejectedA queued review was rejected in the dashboard.
redaction.completedEvery successful POST /redact on a redaction project.

Event payload

Every delivery is a JSON envelope with a stable shape. The event-specific fields live under data.

redaction.completed delivery
POST to your endpoint
{
"id": "evt_01KVMZ7P2YQ9XJ8R5C3F0N2T6B",
"type": "redaction.completed",
"createdAt": "2026-06-22T18:24:11.482Z",
"accountId": "acc_123",
"projectId": "proj_123",
"data": {
"redactionId": "red_01KVMZ6K4RBZ1PAMWJH7EK4XQW",
"imageKey": "accounts/acc_123/projects/proj_123/uploads/profile.jpg",
"redactedImageKey": "accounts/acc_123/projects/proj_123/redacted/01KVMZ6K.jpg",
"style": "blur",
"facesBlurred": 1,
"textBlurred": 2,
"licensePlatesBlurred": 1,
"regions": [/* RedactionRegion[] */],
"createdAt": "2026-06-22T18:24:11.300Z"
}
}

These headers accompany every request:

HeaderDescription
visora-event-idUnique event id (also in the body as id).
visora-event-typeEvent type, e.g. redaction.completed.
visora-timestampUnix seconds when the delivery was signed.
visora-signatureSignature in the form v1=<hex>.

Verify signatures

The signature is v1=<hex> where the HMAC SHA-256 is computed over `${timestamp}.${rawBody}` using your endpoint secret. Verify against the raw request body — not a re-serialized object.

Verify a delivery
import { verifyWebhookSignature } from "@visoracloud/client";
 
const valid = verifyWebhookSignature({
secret: process.env.VISORA_WEBHOOK_SECRET!,
payload: rawBody, // the exact raw request body string
timestamp: req.headers["visora-timestamp"],
signature: req.headers["visora-signature"],
});
 
if (!valid) {
return res.status(401).json({ error: "Invalid signature" });
}

SDK handlers

Install @visoracloud/client. The framework handlers verify the signature, parse the event, and narrow event.data by event.type so no casting is required.

Next.js route handler
import { createNextWebhookHandler } from "@visoracloud/client";
 
// Verifies the signature and parses the event for you.
export const POST = createNextWebhookHandler({
secret: process.env.VISORA_WEBHOOK_SECRET!,
onEvent: async (event) => {
switch (event.type) {
case "moderation.completed":
await onModerated(event.data.moderationId, event.data.action);
break;
case "moderation.review_required":
await notifyReviewTeam(event.data.reviewId);
break;
case "review.approved":
case "review.rejected":
await syncReviewDecision(event.data.reviewId, event.type);
break;
case "redaction.completed":
// event.data is narrowed to VisoraRedactionCompletedData
await storeRedactedImage(event.data.redactionId, event.data.redactedImageKey);
break;
}
},
});
Express handler (raw body required)
import express from "express";
import { createExpressWebhookHandler } from "@visoracloud/client";
 
const app = express();
 
// Visora handlers need the raw body — mount express.raw() on the route.
app.post(
"/webhooks/visora",
express.raw({ type: "application/json" }),
createExpressWebhookHandler({
secret: process.env.VISORA_WEBHOOK_SECRET!,
onEvent: async (event) => {
if (event.type === "redaction.completed") {
await storeRedactedImage(event.data.redactionId);
}
},
}),
);

Prefer to verify and parse manually? Use constructWebhookEvent, which throws on an invalid signature.

Manual verification
import { constructWebhookEvent } from "@visoracloud/client";
 
// Throws VisoraWebhookSignatureError if the signature is invalid.
const event = constructWebhookEvent({
secret: process.env.VISORA_WEBHOOK_SECRET!,
payload: rawBody,
timestamp: req.headers["visora-timestamp"],
signature: req.headers["visora-signature"],
});

Secret rotation

Rotate a signing secret from the webhook detail page. The previous secret stays valid for 24 hours so in-flight deliveries keep verifying. Accept both during the window:

Verify during rotation
// During rotation, accept the current and previous secret for 24 hours.
const valid = verifyWebhookSignature({
secret: [
process.env.VISORA_WEBHOOK_SECRET!,
process.env.VISORA_PREVIOUS_WEBHOOK_SECRET!,
],
payload: rawBody,
timestamp,
signature,
});

Delivery semantics

PropertyBehavior
AsyncDeliveries never block the /moderate or /redact response.
SignedHMAC SHA-256 over `timestamp.rawBody` with your endpoint secret.
RetriedFailed deliveries are retried with backoff.
Dead-letterAfter repeated failures the event moves to a DLQ; you can retry it from the dashboard.
Per projectEach endpoint belongs to a project and only receives that project's events.

Respond with a 2xx quickly to acknowledge receipt. Non-2xx responses and timeouts are retried; persistent failures land in the dead-letter queue, where you can inspect and retry them from Dashboard → Webhooks.

← PreviousRedaction docsNext →Dashboard