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
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.
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.
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.
verifyWebhookSignature() validates visora-signature against the raw body and timestamp.createNextWebhookHandler() and createExpressWebhookHandler() parse, verify, and dispatch events.VisoraWebhookEvent narrows event.data based on event.type.| Export | Use it for |
|---|---|
| verifyWebhookSignature | Manual verification when you already own request parsing. |
| constructWebhookEvent | Verify the signature and parse the raw payload into a typed event. |
| createNextWebhookHandler | Next.js App Router route handlers that return 401 on invalid signatures. |
| createExpressWebhookHandler | Express routes using express.raw({ type: "application/json" }). |
| VisoraWebhookEventType | Event type unions for moderation and review events. |
| VisoraWebhookEvent | Typed webhook envelope with narrowed data payloads. |
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.
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
moderateImage. This uploads and scans in one call.allow, review, or reject to control your product flow.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.
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.
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.
| Field | Type | How to use it |
|---|---|---|
| moderationId | string | Store this id for audit trails, support, review queue lookups, and customer debugging. |
| safe | boolean | Convenience boolean. true only when final action is allow. |
| action | allow | review | reject | Primary field for product logic. |
| riskScore | number | 0-100 score from matched labels and policy thresholds. |
| category | string | null | Main normalized category that drove the decision. |
| explanation | object | Developer-friendly reason for the decision. |
| labels | array | Detected moderation and supplemental labels with confidence and normalized category. |
| brandSafety | object | safe, caution, or unsafe assessment for brand suitability. |
| compliance | object | null | Pack-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 message | Meaning |
|---|---|
| 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.
/moderatex-api-key requiredimage 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./upload-urlx-api-key required/moderation-logsx-api-key required/healthpublic{"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.
| Concept | Behavior |
|---|---|
| Project setting | reviewEnabled controls whether review decisions enter manual review or fall back to a yes/no action. |
| Queue scope | Review items are scoped by account and project. The dashboard sidebar shows the count. |
| Image previews | Rejected/review-sensitive images can be blurred in the dashboard and revealed intentionally. |
| Developer flow | Treat 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.
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.
verifyWebhookSignature() or a framework helper from @visoracloud/client.Webhook event types
Webhook payloads share the same top-level envelope: id, type, createdAt, accountId, projectId, and data.
| Event | When it fires | Typical use |
|---|---|---|
| moderation.completed | After a successful POST /moderate request. | Sync the final action, risk score, explanation, labels, brand safety, and compliance result to your app. |
| moderation.review_required | When a moderation result creates a review queue item. | Notify reviewers, open an internal task, or mark your own image record as pending. |
| review.approved | When a queued review item is approved in the dashboard. | Publish or unlock an image that was waiting for human review. |
| review.rejected | When a queued review item is rejected in the dashboard. | Remove, hide, or keep blocking an image after human review. |
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.
| Header | Description |
|---|---|
| visora-event-id | Unique event id. Store it to make your handler idempotent. |
| visora-event-type | One of moderation.completed, moderation.review_required, review.approved, or review.rejected. |
| visora-timestamp | Unix timestamp used in the signature input. |
| visora-signature | HMAC SHA-256 signature in v1=<hex> format. Verify with verifyWebhookSignature(). |
| Delivery status | Meaning |
|---|---|
| pending | The event has been created and is waiting for delivery. |
| delivered | The endpoint returned a successful 2xx response. |
| failed | The endpoint returned a non-2xx response or timed out. |
| skipped | No 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.
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.
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
| Pack | Typical use | Strict areas |
|---|---|---|
| marketplace | Commerce listings and user-generated product images. | nudity, weapons, drugs, hate symbols; violence usually review. |
| kids | Child-safe products or communities. | Most sensitive categories reject; alcohol/gambling review. |
| education | Learning platforms and classroom content. | nudity, suggestive, weapons, drugs, hate symbols. |
| social | Social feeds and community apps. | Hard-block hate symbols; review several risky categories. |
| dating | Dating and profile content. | Reject violence, weapons, drugs, hate symbols; suggestive can allow. |
| ads | Ad 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:
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.
| Plan | Price | Moderations | Projects | API keys | Retention | Overage |
|---|---|---|---|---|---|---|
| Free | $0 | 500/month | 1 | 1 | 7 days | Blocked at limit |
| Starter | $29/month | 10,000/month | 3 | 3 | 30 days | $4 per 1k overage |
| Growth | $149/month | 50,000/month | 10 | 10 | 90 days | $3 per 1k overage |
| Scale | $399/month | 150,000/month | 50 | 50 | 180 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.
| Status | Meaning | When it happens |
|---|---|---|
| 400 | Bad request | Invalid JSON, missing imageKey, missing file, unsupported content type, or file too large. |
| 401 | Unauthorized | Missing or invalid x-api-key, or missing/invalid Cognito Bearer token on dashboard routes. |
| 403 | Forbidden | API key is inactive/revoked or the imageKey does not belong to the authenticated project. |
| 404 | Not found | Route does not exist. |
| 429 | Limit exceeded | Free plan monthly limit has been reached. Paid plans can continue and accrue overage usage. |
| 500 | Internal server error | Unexpected DynamoDB, S3, Rekognition, or Lambda failure. |
| SDK error | Use case |
|---|---|
| VisoraApiError | Base class for non-2xx API responses. |
| VisoraAuthError | 401 or 403 authentication/authorization failures. |
| VisoraRateLimitError | 429 monthly usage limit exceeded. |
| VisoraValidationError | 400 request validation failures. |
| VisoraTimeoutError | Client-side request timeout. |