VisoraDocs
APIDashboard
VISORA DEVELOPER DOCS

Integrate image moderation into your app

Visora gives your backend one moderation decision per image: allow, review, or reject. Use the Node SDK for uploads, direct REST calls for custom stacks, and project policies to control what your product accepts.

Current API base URL: https://6p0ws7vu2f.execute-api.us-east-1.amazonaws.com/dev

i

Treat Visora API keys like server secrets. Call Visora from your backend, API route, worker, or server action. Do not put x-api-key in frontend JavaScript, mobile clients, or public repositories.

Backend-first integration
Your UI sends a file to your backend. Your backend calls Visora, stores the result, then decides whether to publish, queue, or block the image.
Project scoped security
Each API key belongs to a project. Uploaded S3 objects are scoped to that account/project and cannot be moderated by another project.
Human-readable decisions
Every moderation can include labels, risk score, brand safety, compliance, and a policy explanation like why weapons caused reject.

1Install the SDK

Use the official Node and TypeScript package. It wraps multipart uploads, JSON image-key moderation, logs, errors, timeouts, and webhook verification helpers. Webhook helpers are available in SDK v0.2 and newer.

$ npm install @visoracloud/client

SDK v0.2 webhook helpers

The SDK includes framework-safe helpers for signed webhook deliveries. Use them in backend routes only; webhook signing secrets must never be shipped to browser code.

Signature verification
verifyWebhookSignature() validates visora-signature against the raw body and timestamp.
Framework helpers
createNextWebhookHandler() and createExpressWebhookHandler() parse, verify, and dispatch events.
Typed events
VisoraWebhookEvent narrows event.data based on event.type.
ExportUse it for
verifyWebhookSignatureManual verification when you already own request parsing.
constructWebhookEventVerify the signature and parse the raw payload into a typed event.
createNextWebhookHandlerNext.js App Router route handlers that return 401 on invalid signatures.
createExpressWebhookHandlerExpress routes using express.raw({ type: "application/json" }).
VisoraWebhookEventTypeEvent type unions for moderation and review events.
VisoraWebhookEventTyped webhook envelope with narrowed data payloads.
SDK webhook imports
import {
constructWebhookEvent,
createExpressWebhookHandler,
createNextWebhookHandler,
verifyWebhookSignature,
type VisoraWebhookEvent,
type VisoraWebhookEventType,
} from "@visoracloud/client";
Typed event handling
import type { VisoraWebhookEvent, VisoraWebhookEventType } from "@visoracloud/client";
 
const eventTypes: VisoraWebhookEventType[] = [
"moderation.completed",
"moderation.review_required",
"review.approved",
"review.rejected",
];
 
async function handleVisoraEvent(event: VisoraWebhookEvent) {
switch (event.type) {
case "moderation.completed":
await saveModerationResult(event.data.moderationId, event.data.action);
break;
case "moderation.review_required":
await createInternalReviewTask(event.data.reviewId, event.data.imageKey);
break;
case "review.approved":
await publishImage(event.data.moderationId);
break;
case "review.rejected":
await hideImage(event.data.moderationId, event.data.decisionReason);
break;
}
}

2Authenticate from server code

Create a project in the dashboard, generate an API key, and store it as an environment variable in your backend runtime.

.env
VISORA_API_KEY=sk_test_...
VISORA_API_URL=https://6p0ws7vu2f.execute-api.us-east-1.amazonaws.com/dev

The SDK defaults to the public Visora API URL. Pass baseUrl only when you need a staging or custom API Gateway stage.

Recommended integration flow

1. User uploads
Your product receives the image in a backend route, not directly in browser code that contains secrets.
2. Call Visora
Send the file with moderateImage. This uploads and scans in one call.
3. Branch on action
Use allow, review, or reject to control your product flow.
4. Store ids
Persist moderationId, action, and explanation.message next to your own image record.

3Moderate your first image

Use multipart moderation for most apps. It avoids making your integration manually request an upload URL and then call moderation separately.

import { readFile } from "node:fs/promises";
import { Visora } from "@visoracloud/client";
 
const visora = new Visora({
apiKey: process.env.VISORA_API_KEY!,
});
 
const file = await readFile("./image.jpg");
const result = await visora.moderateImage({
file: file,
filename: "image.jpg",
contentType: "image/jpeg",
});
 
if (result.action === "reject") {
throw new Error(result.explanation?.message ?? "Image rejected");
}
 
console.log(result.action, result.riskScore, result.explanation?.message);

Handle decisions in your product

Your application should treat moderation as a product decision, not only as a list of labels. The stable field to branch on is action.

Allow
Continue the user flow
The image did not violate the active project policy, or the matched category is explicitly configured as allow.
Review
Queue for manual decision
Borderline or policy-specific content. When review mode is enabled, this can be handled from the project review queue.
Reject
Block automatically
The image matched a category, threshold, or compliance-sensitive condition that the project policy treats as reject.
Application branching
switch (result.action) {
case "allow":
await publishImage(result.moderationId);
break;
case "review":
await markAsPendingReview(result.moderationId, result.explanation?.message);
break;
case "reject":
await blockImageUpload(result.explanation?.message ?? "Image rejected");
break;
}

Moderation response

POST /moderate is backward-compatible. Existing integrations can keep using safe, action, and labels; newer integrations can use policy explanations, risk score, brand safety, and compliance.

200 OKapplication/json
{
"moderationId": "mod_01KV9F2X3Q",
"safe": false,
"action": "reject",
"riskScore": 92,
"category": "weapons",
"explanation": {
"message": "Rejected because weapons matched reject action.",
"reason": "category_action", "matchedCategory": "weapons"
},
"labels": [
{ "name": "Weapon", "confidence": 93.14, "category": "weapons" }
],
"brandSafety": { "safe": false, "score": 92, "level": "unsafe", "reasons": ["weapons"] },
"compliance": { "pack": "marketplace", "passed": false, "violations": ["weapons"] }
}
FieldTypeHow to use it
moderationIdstringStore this id for audit trails, support, review queue lookups, and customer debugging.
safebooleanConvenience boolean. true only when final action is allow.
actionallow | review | rejectPrimary field for product logic.
riskScorenumber0-100 score from matched labels and policy thresholds.
categorystring | nullMain normalized category that drove the decision.
explanationobjectDeveloper-friendly reason for the decision.
labelsarrayDetected moderation and supplemental labels with confidence and normalized category.
brandSafetyobjectsafe, caution, or unsafe assessment for brand suitability.
complianceobject | nullPack-specific pass/fail result when a compliance pack is configured.

Policy decision explanations

Use explanation.message for dashboards, admin tools, and support logs. It explains the policy decision without requiring your team to reverse-engineer labels.

Example messageMeaning
Rejected because weapons matched reject action.The project has weapons configured as reject and a matching label crossed confidence rules.
Allowed because nudity matched allow action.The label is still returned, but the project policy explicitly allows that category.
Rejected because review is disabled and violence matched review action.Review mode is off for the project, so review decisions fall back to the configured yes/no behavior.
Allowed because no configured moderation categories matched this image.No policy category or threshold required review/reject.

Customer API reference

These routes are for customer integrations and use x-api-key. They are metered against the account monthly usage limit.

POST/moderatex-api-key required
Accepts multipart field image or JSON body { "imageKey": "..." }. Multipart is recommended because it uploads and scans in one request. JSON image keys must belong to the authenticated account/project prefix.
Multipart upload
200 OK
curl -X POST "$VISORA_API_URL/moderate" \
-H "x-api-key: $VISORA_API_KEY" \
-F "image=@./image.jpg;type=image/jpeg"
Existing S3 object
200 OK
curl -X POST "$VISORA_API_URL/moderate" \
-H "content-type: application/json" \
-H "x-api-key: $VISORA_API_KEY" \
-d '{"imageKey":"accounts/acc_123/projects/proj_123/uploads/image.jpg"}'
POST/upload-urlx-api-key required
Returns a 5 minute S3 presigned PUT URL and a scoped image key. Use this only when you specifically need direct S3 upload before moderation.
Create upload URL
200 OK
curl -X POST "$VISORA_API_URL/upload-url" \
-H "x-api-key: $VISORA_API_KEY"
GET/moderation-logsx-api-key required
Returns recent moderation logs for the authenticated project only. Logs include labels, decision fields, explanation, brand safety, compliance, imageKey, imageUrl when available, and createdAt.
List logs
200 OK
curl "$VISORA_API_URL/moderation-logs" \
-H "x-api-key: $VISORA_API_KEY"
GET/healthpublic
Returns {"status":"ok"}. Use it for uptime checks.

Review queue

When a project has review mode enabled, review decisions can be sent to the project review queue. This lets a human approve or reject specific images from the dashboard without changing the public moderation response contract.

ConceptBehavior
Project settingreviewEnabled controls whether review decisions enter manual review or fall back to a yes/no action.
Queue scopeReview items are scoped by account and project. The dashboard sidebar shows the count.
Image previewsRejected/review-sensitive images can be blurred in the dashboard and revealed intentionally.
Developer flowTreat action=review as pending in your app, then resolve it with your internal admin process.

Webhooks

Use webhooks to receive moderation and review events in your backend without polling. Webhooks are configured per project from the dashboard, and each endpoint subscribes only to the event types you select.

i

Copy the webhook signing secret when you create the endpoint. Visora only shows it once. Store it as VISORA_WEBHOOK_SECRET in your server environment and verify every delivery before trusting the payload.

1. Create endpoint
Open a project, add your HTTPS endpoint URL, select the subscribed events, then copy the signing secret.
2. Verify signature
Use verifyWebhookSignature() or a framework helper from @visoracloud/client.
3. Return 2xx
Return any 2xx status after processing. Non-2xx responses are treated as delivery failures and retried.

Webhook event types

Webhook payloads share the same top-level envelope: id, type, createdAt, accountId, projectId, and data.

EventWhen it firesTypical use
moderation.completedAfter a successful POST /moderate request.Sync the final action, risk score, explanation, labels, brand safety, and compliance result to your app.
moderation.review_requiredWhen a moderation result creates a review queue item.Notify reviewers, open an internal task, or mark your own image record as pending.
review.approvedWhen a queued review item is approved in the dashboard.Publish or unlock an image that was waiting for human review.
review.rejectedWhen a queued review item is rejected in the dashboard.Remove, hide, or keep blocking an image after human review.
Webhook payload
POST
{
"id": "evt_01KW0WEBHOOK7R6Z4C2J5K8",
"type": "moderation.completed",
"createdAt": "2026-06-19T14:32:08.000Z",
"accountId": "acc_123",
"projectId": "proj_123",
"data": {
"moderationId": "mod_01KV9F2X3Q",
"imageKey": "accounts/acc_123/projects/proj_123/uploads/image.jpg",
"safe": false,
"action": "reject",
"riskScore": 92,
"category": "weapons",
"labels": [
{ "name": "Weapon", "confidence": 93.14, "category": "weapons" }
],
"explanation": {
"message": "Rejected because weapons matched reject action."
}
}
}

Verify webhook signatures

Every delivery includes signing headers. The SDK verifies the HMAC input visora-timestamp + "." + rawBody, supports a five minute replay window by default, and can accept both current and previous secrets during rotation. If verification fails, the Next.js helper returns 401; the Express helper returns 401 unless you pass your own error middleware.

Delivery headers
Content-Type: application/json
User-Agent: Visora-Webhooks/1.0
visora-event-id: evt_01KW0WEBHOOK7R6Z4C2J5K8
visora-event-type: moderation.completed
visora-timestamp: 1781889128
visora-signature: v1=4e8c...
verifyWebhookSignature()
import { verifyWebhookSignature } from "@visoracloud/client";
 
const valid = verifyWebhookSignature({
secret: process.env.VISORA_WEBHOOK_SECRET!,
payload: rawBody,
timestamp: request.headers["visora-timestamp"] as string,
signature: request.headers["visora-signature"] as string,
});
 
if (!valid) {
throw new Error("Invalid Visora webhook signature");
}
Next.js webhook route
import { createNextWebhookHandler } from "@visoracloud/client";
 
export const POST = createNextWebhookHandler({
secret: process.env.VISORA_WEBHOOK_SECRET!,
async onEvent(event) {
switch (event.type) {
case "moderation.completed":
await updateImageModeration(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;
}
},
});
Express webhook route
import express from "express";
import { createExpressWebhookHandler } from "@visoracloud/client";
 
const app = express();
 
app.post(
"/webhooks/visora",
express.raw({ type: "application/json" }),
createExpressWebhookHandler({
secret: process.env.VISORA_WEBHOOK_SECRET!,
async onEvent(event) {
if (event.type === "moderation.completed") {
await updateImageModeration(event.data.moderationId, event.data.action);
}
},
})
);
Secret rotation window
import { constructWebhookEvent } from "@visoracloud/client";
 
const event = constructWebhookEvent({
secret: [
process.env.VISORA_WEBHOOK_SECRET!,
process.env.VISORA_PREVIOUS_WEBHOOK_SECRET!,
].filter(Boolean),
payload: rawBody,
timestamp,
signature,
});
HeaderDescription
visora-event-idUnique event id. Store it to make your handler idempotent.
visora-event-typeOne of moderation.completed, moderation.review_required, review.approved, or review.rejected.
visora-timestampUnix timestamp used in the signature input.
visora-signatureHMAC SHA-256 signature in v1=<hex> format. Verify with verifyWebhookSignature().
Delivery statusMeaning
pendingThe event has been created and is waiting for delivery.
deliveredThe endpoint returned a successful 2xx response.
failedThe endpoint returned a non-2xx response or timed out.
skippedNo active webhook endpoint was subscribed to the event type.

Project policies

Policies live inside each project and control the main action, safe, riskScore, and category. Category actions take priority over generic thresholds.

Project policy example
{
"reviewEnabled": true,
"reviewDisabledAction": "reject",
"minConfidence": 70,
"reviewThreshold": 50,
"rejectThreshold": 80,
"blockedCategories": ["nudity", "violence", "weapons", "drugs"],
"categoryActions": {
"nudity": "allow",
"violence": "review",
"weapons": "reject",
"drugs": "reject"
},
"compliancePack": "marketplace"
}

If a project sets nudity: allow, nudity labels still appear in responses and logs, but the final action can be allow for that project.

Supported categories: nudity suggestive violence weapons drugs hate_symbols gambling alcohol

Brand safety

Brand safety is an additional evaluation layer based on the final action, risk score, and detected categories. Use it when your product needs a simpler safe/caution/unsafe signal for business workflows.

safe
Final action is allow and no unsafe brand categories are present.
caution
Final action is review, or detected content is sensitive but not hard-blocked by policy.
unsafe
Final action is reject, or categories like weapons, drugs, hate symbols, violence, or nudity make it unsuitable.

Compliance packs

Compliance packs are presets evaluated in addition to the active project policy. They do not replace the main project action; they report whether the image passes a use-case-specific standard.

Supported packs: marketplace kids education social dating ads

PackTypical useStrict areas
marketplaceCommerce listings and user-generated product images.nudity, weapons, drugs, hate symbols; violence usually review.
kidsChild-safe products or communities.Most sensitive categories reject; alcohol/gambling review.
educationLearning platforms and classroom content.nudity, suggestive, weapons, drugs, hate symbols.
socialSocial feeds and community apps.Hard-block hate symbols; review several risky categories.
datingDating and profile content.Reject violence, weapons, drugs, hate symbols; suggestive can allow.
adsAd creative checks.Strict on nudity, violence, weapons, drugs; alcohol/gambling review.

Scoped uploads

Uploaded images are owned by the account and project from the authenticated API key. Generated keys use this format:

S3 key format
accounts/{accountId}/projects/{projectId}/uploads/{ulid}.jpg

POST /moderate rejects image keys outside the authenticated prefix with 403 and You do not have access to this image. This check avoids extra S3 or DynamoDB reads.

Plans, limits, and retention

Plans control monthly usage, project/key limits, overage behavior, and S3/log retention. Free plans block at the monthly limit; paid plans can continue and track overage.

PlanPriceModerationsProjectsAPI keysRetentionOverage
Free$0500/month117 daysBlocked at limit
Starter$29/month10,000/month3330 days$4 per 1k overage
Growth$149/month50,000/month101090 days$3 per 1k overage
Scale$399/month150,000/month5050180 days$2.50 per 1k overage

Errors and retries

Errors are returned as JSON with a clear error message. The SDK maps common failures to typed errors so integrations can handle auth, validation, limits, and timeouts cleanly.

StatusMeaningWhen it happens
400Bad requestInvalid JSON, missing imageKey, missing file, unsupported content type, or file too large.
401UnauthorizedMissing or invalid x-api-key, or missing/invalid Cognito Bearer token on dashboard routes.
403ForbiddenAPI key is inactive/revoked or the imageKey does not belong to the authenticated project.
404Not foundRoute does not exist.
429Limit exceededFree plan monthly limit has been reached. Paid plans can continue and accrue overage usage.
500Internal server errorUnexpected DynamoDB, S3, Rekognition, or Lambda failure.
SDK error handling
import { VisoraRateLimitError, VisoraTimeoutError } from "@visoracloud/client";
 
try {
const result = await visora.moderateImage({ file, filename, contentType });
return result;
} catch (error) {
if (error instanceof VisoraRateLimitError) {
return { error: "Monthly moderation limit exceeded" };
}
if (error instanceof VisoraTimeoutError) {
// Safe to retry once from your server worker or request handler.
}
throw error;
}
SDK errorUse case
VisoraApiErrorBase class for non-2xx API responses.
VisoraAuthError401 or 403 authentication/authorization failures.
VisoraRateLimitError429 monthly usage limit exceeded.
VisoraValidationError400 request validation failures.
VisoraTimeoutErrorClient-side request timeout.

Next steps

Create a project
Projects own API keys, policies, usage, scoped uploads, review queue, and moderation logs.
Generate an API key
Use project API keys from server code only. Never ship them to a browser, mobile app, or public repo.
Wire the SDK
Call moderateImage from your backend upload route and branch on allow, review, or reject.
Tune policy
Configure category actions, thresholds, review mode, brand safety, and compliance packs per project.
Add webhooks
Send moderation and review events to your backend for automation, notifications, and audit workflows.
← PreviousPricingNext →Dashboard