REST API
Direct HTTP API reference for Vettly moderation.
Base URL
https://api.vettly.devAuthentication
All requests require an API key in the Authorization header:
http
Authorization: Bearer vettly_xxxxxxxxxxxxxContent Moderation
Check Content
Check content for moderation violations.
http
POST /v1/checkRequest
json
{
"content": "Text to moderate",
"policyId": "moderate",
"contentType": "text",
"metadata": {
"userId": "user_123",
"ip": "192.168.1.1"
}
}Response
json
{
"safe": true,
"flagged": false,
"action": "allow",
"categories": [
{
"category": "violence",
"score": 0.05,
"threshold": 0.7,
"triggered": false
},
{
"category": "sexual",
"score": 0.02,
"threshold": 0.75,
"triggered": false
}
],
"decisionId": "dec_abc123xyz",
"provider": "openai",
"latency": 234,
"cost": 0.001
}cURL Example
bash
curl -X POST https://api.vettly.dev/v1/check \
-H "Authorization: Bearer vettly_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"content": "Hello, world!",
"policyId": "moderate",
"contentType": "text"
}'Dry Run
Test a policy without calling AI providers.
http
POST /v1/check/dry-runRequest
json
{
"policyId": "moderate",
"mockScores": {
"violence": 0.8,
"sexual": 0.3,
"hate": 0.95
}
}Batch Check (Sync)
Check multiple items synchronously.
http
POST /v1/batch/checkRequest
json
{
"policyId": "moderate",
"items": [
{
"id": "comment_1",
"content": "Great post!",
"contentType": "text"
},
{
"id": "comment_2",
"content": "Inappropriate content",
"contentType": "text"
}
]
}Response
json
{
"items": [
{
"id": "comment_1",
"safe": true,
"action": "allow",
"decisionId": "dec_abc123"
},
{
"id": "comment_2",
"safe": false,
"action": "block",
"decisionId": "dec_def456"
}
]
}Batch Check (Async)
Check multiple items with webhook delivery.
http
POST /v1/batch/check/asyncRequest
json
{
"policyId": "moderate",
"items": [
{ "id": "1", "content": "Text 1" },
{ "id": "2", "content": "Text 2" }
],
"webhookUrl": "https://myapp.com/webhooks/moderation"
}Response
json
{
"batchId": "batch_abc123",
"status": "processing",
"totalItems": 2
}Policies
Create Policy
http
POST /v1/policiesRequest
json
{
"policyId": "my_custom_policy",
"yamlContent": "name: My Policy\ncategories:\n violence:\n threshold: 0.7"
}Get Policy
http
GET /v1/policies/{policyId}List Policies
http
GET /v1/policiesResponse
json
{
"policies": [
{
"policyId": "moderate",
"name": "Moderate Policy",
"categories": { ... }
}
]
}Decisions
Get Decision
http
GET /v1/decisions/{decisionId}Response
json
{
"decisionId": "dec_abc123",
"content": "Original content",
"safe": true,
"action": "allow",
"timestamp": "2025-01-18T10:30:00Z",
"policyId": "moderate"
}List Decisions
http
GET /v1/decisions?limit=100&offset=0Replay Decision
http
POST /v1/decisions/{decisionId}/replayRequest
json
{
"policyId": "strict"
}Get cURL Command
http
GET /v1/decisions/{decisionId}/curlResponse
json
{
"curl": "curl -X POST https://api.vettly.dev/v1/check..."
}Webhooks
Register Webhook
http
POST /v1/webhooksRequest
json
{
"url": "https://myapp.com/webhooks/vettly",
"events": ["decision.created", "decision.flagged", "decision.blocked"],
"description": "Production webhook"
}Response
json
{
"id": "wh_abc123",
"url": "https://myapp.com/webhooks/vettly",
"events": ["decision.created", "decision.flagged"],
"secret": "whsec_xxxxx",
"enabled": true
}List Webhooks
http
GET /v1/webhooksGet Webhook
http
GET /v1/webhooks/{webhookId}Update Webhook
http
PATCH /v1/webhooks/{webhookId}Request
json
{
"enabled": false,
"events": ["decision.created"]
}Delete Webhook
http
DELETE /v1/webhooks/{webhookId}Test Webhook
http
POST /v1/webhooks/{webhookId}/testRequest
json
{
"eventType": "decision.created"
}Get Deliveries
http
GET /v1/webhooks/{webhookId}/deliveries?limit=50Rate Limits
Rate Limit Headers
http
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 299
X-RateLimit-Reset: 1642531200Rate Limit Exceeded
http
HTTP/1.1 429 Too Many Requestsjson
{
"error": "Rate limit exceeded",
"retryAfter": 60
}Error Codes
| Code | Meaning | Solution |
|---|---|---|
| 400 | Bad Request | Check request format |
| 401 | Unauthorized | Check API key |
| 403 | Forbidden | Check API key permissions |
| 404 | Not Found | Check endpoint/resource ID |
| 429 | Too Many Requests | Implement rate limiting |
| 500 | Server Error | Retry or contact support |
Error Response Format
json
{
"error": "Invalid API key",
"code": "INVALID_API_KEY",
"details": "The provided API key is not valid"
}Content Types
Text
json
{
"content": "Text to check",
"contentType": "text"
}Image (Base64)
json
{
"content": "/9j/4AAQSkZJRgABAQAAAQ...",
"contentType": "image"
}Video
Video moderation is handled through dedicated endpoints. See Video Moderation below.
Multi-Modal Moderation
Check multiple content types in a single request.
Multi-Modal Check
http
POST /v1/check/multimodalRequest
json
{
"text": "Caption for the post",
"images": ["https://example.com/image1.jpg", "https://example.com/image2.jpg"],
"policyId": "moderate",
"context": {
"useCase": "social_post",
"userId": "user_123",
"userReputation": 0.85,
"locale": "en-US"
},
"metadata": {
"postId": "post_abc123"
}
}Response
json
{
"decisionId": "dec_abc123",
"safe": true,
"flagged": false,
"action": "allow",
"results": [
{
"contentType": "text",
"safe": true,
"flagged": false,
"action": "allow",
"categories": [...]
},
{
"contentType": "image",
"contentRef": "https://example.com/image1.jpg",
"safe": true,
"flagged": false,
"action": "allow",
"categories": [...]
}
],
"totalLatency": 450,
"totalCost": 0.002
}Video Moderation
Video moderation is asynchronous due to processing time. Results are delivered via webhook.
Check Video by URL
http
POST /v1/video/checkRequest
json
{
"videoUrl": "https://example.com/video.mp4",
"policyId": "moderate",
"webhookUrl": "https://myapp.com/webhooks/video",
"extractionStrategy": {
"frameInterval": 2,
"maxFrames": 50
}
}Response (202 Accepted)
json
{
"decisionId": "dec_video123",
"status": "processing",
"estimatedTime": 30,
"estimatedFrames": 25,
"estimatedCost": 0.025
}Upload Video File
http
POST /v1/video/upload
Content-Type: multipart/form-dataRequest
Form fields:
video: Video file (max 100MB)policyId: Policy IDwebhookUrl: Webhook URL for results (optional)maxDurationSeconds: Max duration (default: 300)
Get Video Status
http
GET /v1/video/status/{decisionId}Response
json
{
"decisionId": "dec_video123",
"status": "processing",
"progress": {
"framesProcessed": 15,
"framesTotal": 25,
"percentComplete": 60
},
"estimatedCompletion": "2025-01-18T10:35:00Z"
}Get Video Result
http
GET /v1/video/result/{decisionId}Response (200 OK when complete)
json
{
"decisionId": "dec_video123",
"status": "completed",
"safe": false,
"flagged": true,
"action": "flag",
"results": [
{
"frameNumber": 1,
"timestamp": 0,
"safe": true,
"flagged": false,
"categories": [...]
},
{
"frameNumber": 15,
"timestamp": 28.5,
"safe": false,
"flagged": true,
"categories": [
{
"category": "violence",
"score": 0.85,
"triggered": true
}
],
"evidence": {
"url": "https://storage.vettly.dev/evidence/frame-15.jpg",
"expiresAt": "2025-01-19T10:30:00Z"
}
}
],
"totalLatency": 28500,
"totalCost": 0.025
}Best Practices
1. Use Idempotency Keys
Prevent duplicate checks:
http
POST /v1/check
Idempotency-Key: unique-request-id2. Handle Retries
Use exponential backoff for failed requests:
javascript
async function checkWithRetry(content, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch('/v1/check', { ... })
} catch (error) {
if (i === maxRetries - 1) throw error
await sleep(1000 * Math.pow(2, i))
}
}
}3. Cache Results
Cache decisions by content hash:
javascript
const cacheKey = sha256(content + policyId)
const cached = cache.get(cacheKey)
if (cached) return cached4. Monitor Costs
Track API usage:
javascript
let totalCost = 0
result.cost && (totalCost += result.cost)
console.log('Total cost today:', totalCost)5. Use Batch Endpoints
For multiple checks:
javascript
// ❌ Don't do this
for (const comment of comments) {
await fetch('/v1/check', { body: comment })
}
// ✅ Do this
await fetch('/v1/batch/check', {
body: { items: comments }
})Webhooks Integration
See Webhooks documentation for details on:
- Event types
- Payload formats
- Signature verification
- Retry logic
SDK vs REST API
Use the SDK when:
- Building Node.js/TypeScript apps
- Want automatic retries and error handling
- Need TypeScript types
- Using Express middleware
Use REST API when:
- Building in other languages (Python, Ruby, Go)
- Need direct HTTP control
- Building serverless functions
- Integrating with no-code tools
Examples
Python
python
import requests
response = requests.post(
'https://api.vettly.dev/v1/check',
headers={
'Authorization': 'Bearer vettly_xxxxx',
'Content-Type': 'application/json'
},
json={
'content': 'Text to check',
'policyId': 'moderate',
'contentType': 'text'
}
)
result = response.json()
print('Safe:', result['safe'])Go
go
package main
import (
"bytes"
"encoding/json"
"net/http"
)
func checkContent(content string) (bool, error) {
payload := map[string]string{
"content": content,
"policyId": "moderate",
"contentType": "text",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest(
"POST",
"https://api.vettly.dev/v1/check",
bytes.NewBuffer(body),
)
req.Header.Set("Authorization", "Bearer vettly_xxxxx")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
// Handle response...
}Ruby
ruby
require 'net/http'
require 'json'
uri = URI('https://api.vettly.dev/v1/check')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path)
request['Authorization'] = 'Bearer vettly_xxxxx'
request['Content-Type'] = 'application/json'
request.body = {
content: 'Text to check',
policyId: 'moderate',
contentType: 'text'
}.to_json
response = http.request(request)
result = JSON.parse(response.body)
puts "Safe: #{result['safe']}"See Also
- TypeScript SDK - Official SDK documentation
- Webhooks - Webhook events and signatures
- Next.js Integration - Next.js API routes
- Express Integration - Express middleware