NSFW Image Detection API
Asynchronous NSFW content detection and image analysis API with callback support
Overview
The NSFW Image Detection API provides asynchronous analysis of images to detect not-safe-for-work (NSFW) content. The API uses advanced AI models to classify images and optionally provide detailed vision-based descriptions.
This API operates asynchronously. Results are delivered via webhook callback to your specified endpoint.
Base URL
https://ext.systems/api/v1/image-checks/nsfwAuthentication
The API uses header-based authentication with temporary API keys.
Authorization: Bearer test_nsfw_xxxxxxRequest Specification
HTTP Method
POST
Content Type
multipart/form-data
Required Parameters
| Parameter | Type | Description |
|---|---|---|
image | File | Image file (JPG/PNG, max 1MB) |
callback_url | String | HTTPS/HTTP URL to receive results |
Optional Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
callback_api_key | String | - | API key for authenticating callback requests |
vision | Boolean | false | Enable detailed image description using vision AI |
File Requirements
- Supported formats: JPG, PNG only - Maximum file size: 1MB - File validation: Non-empty files only - Single file: Exactly one image per request
Response Format
Success Response (202 Accepted)
{
"success": true,
"id": "OrOcqNpFd7oE_84n8oNzT",
"queueId": "a6d3a137-0d49-4895-ba97-29a19fd440c4",
"metadata": {
"processedAt": "2025-07-19T01:10:18.5742Z",
"originalFileName": "76306824.jpeg",
"processedFileName": "nsfw-NEoj583GnWm571YengoUn.jpeg",
"fileSize": 915005,
"fileType": "image/jpeg"
}
}Error Responses
{
"success": false,
"message": "File size exceeds 1MB limit. Please upload a smaller file."
}{
"success": false,
"message": "Failed to process image analysis request"
}Callback Mechanism
When processing completes, the API sends a POST request to your callback_url
with the analysis results.
Callback Authentication
If you provided a callback_api_key, it will be included in the callback
request:
Content-Type: application/json
Authorization: Bearer your_callback_api_keyCallback Payload
{
"id": "OrOcqNpFd7oE_84n8oNzT",
"nsfw": false
}{
"id": "OrOcqNpFd7oE_84n8oNzT",
"nsfw": false,
"vision": "A young woman stands before a swimming pool wearing a black bikini top; her light blonde hair is wet, indicating recent swimming. Her eyes are closed and mouth slightly open, suggesting relaxation or deep thought. She has a subtle glow on her skin, possibly from sunlight or water. In the background, palm trees and greenery hint at a tropical setting, while the clear blue sky suggests pleasant weather. The woman's tranquil pose captures a serene moment in a warm environment."
}{
"id": "OrOcqNpFd7oE_84n8oNzT",
"nsfw": false,
"vision": "Error: Could not analyze image using vision tools"
}Callback Response Fields
| Field | Type | Description |
|---|---|---|
id | String | Unique job identifier from original request |
nsfw | Boolean | true if NSFW content detected, false otherwise |
vision | String | Detailed image description (only if vision: true was requested) |
Usage Examples
cURL Example
curl -X POST \
-H "Authorization: Bearer test_nsfw_xxxxxx" \
-F "image=@/path/to/image.jpg" \
-F "callback_url=https://b5c8e03c8236.ngrok-free.app" \
-F "callback_api_key=auth_qYol8S0bk0O2Y0KCy15" \
-F "vision=true" \
https://ext.systems/api/v1/image-checks/nsfwJavaScript/Fetch Example
async function checkImageNSFW(imageFile, callbackUrl, apiKey) {
const formData = new FormData();
formData.append('image', imageFile);
formData.append('callback_url', callbackUrl);
formData.append('callback_api_key', apiKey);
formData.append('vision', 'true');
try {
const response = await fetch(
'https://ext.systems/api/v1/image-checks/nsfw',
{
method: 'POST',
headers: {
Authorization: 'Bearer test_nsfw_xxxxxx',
},
body: formData,
},
);
const result = await response.json();
if (result.success) {
console.log('Image submitted for analysis:', result.id);
return result;
} else {
throw new Error(result.message);
}
} catch (error) {
console.error('Upload failed:', error);
throw error;
}
}
// Usage
const fileInput = document.getElementById('imageInput');
const file = fileInput.files[0];
checkImageNSFW(
file,
'https://your-api.com/webhooks/nsfw-result',
'your-callback-api-key',
);Node.js Example
import FormData from 'form-data';
import fetch from 'node-fetch';
import fs from 'fs';
async function submitImageForNSFWCheck(imagePath, callbackUrl) {
const form = new FormData();
// Add image file
form.append('image', fs.createReadStream(imagePath));
// Add callback configuration
form.append('callback_url', callbackUrl);
form.append('callback_api_key', process.env.CALLBACK_API_KEY);
form.append('vision', 'false'); // Disable vision for faster processing
const response = await fetch('https://ext.systems/api/v1/image-checks/nsfw', {
method: 'POST',
headers: {
Authorization: 'Bearer test_nsfw_xxxxxx',
...form.getHeaders(),
},
body: form,
});
return await response.json();
}Python Example
import requests
def check_nsfw_image(image_path, callback_url, vision=False):
url = "https://ext.systems/api/v1/image-checks/nsfw"
headers = {
"Authorization": "Bearer test_nsfw_xxxxxx"
}
files = {
"image": open(image_path, "rb")
}
data = {
"callback_url": callback_url,
"vision": str(vision).lower()
}
try:
response = requests.post(url, headers=headers, files=files, data=data)
response.raise_for_status()
return response.json()
finally:
files["image"].close()
# Usage
result = check_nsfw_image(
"path/to/image.jpg",
"https://your-webhook-endpoint.com/nsfw-callback",
vision=True
)
print(f"Job ID: {result['id']}")PHP/Symfony Example
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
readonly class NSFWDetectionService
{
private const string API_URL = 'https://ext.systems/api/v1/image-checks/nsfw';
public function __construct(
private HttpClientInterface $httpClient,
private string $apiKey,
) {}
/**
* @throws NSFWAnalysisException
*/
public function checkImage(
File $imageFile,
string $callbackUrl,
?string $callbackApiKey = null,
bool $vision = false
): NSFWAnalysisResult {
$formData = [
'image' => fopen($imageFile->getPathname(), 'r'),
'callback_url' => $callbackUrl,
'vision' => match ($vision) {
true => 'true',
false => 'false',
},
];
if ($callbackApiKey !== null) {
$formData['callback_api_key'] = $callbackApiKey;
}
try {
$response = $this->httpClient->request(
method: 'POST',
url: self::API_URL,
options: [
'headers' => [
'Authorization' => "Bearer {$this->apiKey}",
],
'body' => $formData,
]
);
return NSFWAnalysisResult::fromArray($response->toArray());
} catch (ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface $e) {
throw new NSFWAnalysisException(
message: "Failed to submit image for NSFW analysis: {$e->getMessage()}",
previous: $e
);
}
}
}
final readonly class NSFWAnalysisResult
{
public function __construct(
public string $id,
public string $queueId,
public array $metadata,
public bool $success = true,
) {}
public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
queueId: $data['queueId'],
metadata: $data['metadata'],
success: $data['success'] ?? true,
);
}
}
final class NSFWAnalysisException extends \RuntimeException {}<?php
declare(strict_types=1);
namespace App\Controller;
use App\Service\NSFWAnalysisException;
use App\Service\NSFWDetectionService;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
enum AllowedMimeType: string
{
case JPEG = 'image/jpeg';
case PNG = 'image/png';
public static function isValid(string $mimeType): bool
{
return in_array($mimeType, array_column(self::cases(), 'value'));
}
}
#[Route('/api', name: 'api_')]
final class ImageController extends AbstractController
{
private const int MAX_FILE_SIZE = 1048576; // 1MB
public function __construct(
private readonly NSFWDetectionService $nsfwService,
private readonly LoggerInterface $logger,
#[Autowire('%env(CALLBACK_API_KEY)%')]
private readonly ?string $callbackApiKey = null,
) {}
#[Route('/upload-image', name: 'upload_image', methods: ['POST'])]
public function uploadImage(Request $request): JsonResponse
{
$uploadedFile = $request->files->get('image');
if (!$uploadedFile instanceof UploadedFile) {
return $this->json(
data: ['error' => 'No image file provided'],
status: Response::HTTP_BAD_REQUEST
);
}
$validationError = $this->validateUploadedFile($uploadedFile);
if ($validationError !== null) {
return $this->json(
data: ['error' => $validationError],
status: Response::HTTP_BAD_REQUEST
);
}
try {
$result = $this->nsfwService->checkImage(
imageFile: new File($uploadedFile->getPathname()),
callbackUrl: 'https://your-domain.com/api/nsfw-callback',
callbackApiKey: $this->callbackApiKey,
vision: $request->request->getBoolean('vision', false)
);
return $this->json($result);
} catch (NSFWAnalysisException $e) {
$this->logger->error('NSFW analysis failed', [
'error' => $e->getMessage(),
'file' => $uploadedFile->getClientOriginalName(),
]);
return $this->json(
data: ['error' => 'Failed to process image'],
status: Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
#[Route('/nsfw-callback', name: 'nsfw_callback', methods: ['POST'])]
public function handleCallback(Request $request): JsonResponse
{
if (!$this->isValidCallback($request)) {
return $this->json(
data: ['error' => 'Unauthorized'],
status: Response::HTTP_UNAUTHORIZED
);
}
$payload = $request->getPayload();
if (!$payload->has('id') || !$payload->has('nsfw')) {
return $this->json(
data: ['error' => 'Invalid payload'],
status: Response::HTTP_BAD_REQUEST
);
}
$this->processNSFWResult(
jobId: $payload->getString('id'),
isNsfw: $payload->getBoolean('nsfw'),
description: $payload->getString('vision', null)
);
return $this->json(['success' => true]);
}
private function validateUploadedFile(UploadedFile $file): ?string
{
return match (true) {
!AllowedMimeType::isValid($file->getMimeType() ?? '') =>
'Only JPG and PNG files allowed',
$file->getSize() > self::MAX_FILE_SIZE =>
'File size exceeds 1MB limit',
default => null,
};
}
private function isValidCallback(Request $request): bool
{
if ($this->callbackApiKey === null) {
return true;
}
$authHeader = $request->headers->get('Authorization', '');
$providedKey = str_replace('Bearer ', '', $authHeader);
return hash_equals($this->callbackApiKey, $providedKey);
}
private function processNSFWResult(string $jobId, bool $isNsfw, ?string $description): void
{
$logContext = [
'job_id' => $jobId,
'is_nsfw' => $isNsfw,
'has_description' => $description !== null,
];
if ($isNsfw) {
$this->logger->warning('NSFW content detected', $logContext);
// Handle NSFW content: flag, notify moderators, etc.
} else {
$this->logger->info('Safe content confirmed', $logContext);
// Approve content: publish, notify user, etc.
}
// Example: Update database record
// $this->entityManager->getRepository(ImageAnalysis::class)
// ->updateAnalysisResult($jobId, $isNsfw, $description);
}
}# config/services.yaml
services:
App\Service\NSFWDetectionService:
arguments:
$httpClient: '@http_client'
$apiKey: '%env(NSFW_API_KEY)%'# .env
NSFW_API_KEY=test_nsfw_xxxxxx
CALLBACK_API_KEY=your_secure_callback_keyCallback Handler Examples
Express.js Webhook Handler
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/nsfw-result', (req, res) => {
const {id, nsfw, vision} = req.body;
// Verify callback API key if you provided one
const expectedApiKey = process.env.CALLBACK_API_KEY;
const providedApiKey = req.headers.authorization?.replace('Bearer ', '');
if (expectedApiKey && providedApiKey !== expectedApiKey) {
return res.status(401).json({error: 'Unauthorized'});
}
console.log(`Image ${id} analysis complete:`);
console.log(`NSFW: ${nsfw}`);
if (vision) {
console.log(`Description: ${vision}`);
}
// Process the result
processNSFWResult(id, nsfw, vision);
res.json({success: true});
});
function processNSFWResult(jobId, isNsfw, description) {
// Update your database, notify users, etc.
if (isNsfw) {
console.log(`⚠️ NSFW content detected in job ${jobId}`);
// Handle NSFW content appropriately
} else {
console.log(`✅ Safe content confirmed for job ${jobId}`);
// Approve content for publication
}
}Next.js API Route Handler
import {NextRequest, NextResponse} from 'next/server';
interface NSFWResult {
id: string;
nsfw: boolean;
vision?: string;
}
export async function POST(request: NextRequest) {
try {
// Verify authorization if callback API key was provided
const authHeader = request.headers.get('authorization');
const expectedKey = process.env.CALLBACK_API_KEY;
if (expectedKey) {
const providedKey = authHeader?.replace('Bearer ', '');
if (providedKey !== expectedKey) {
return NextResponse.json({error: 'Unauthorized'}, {status: 401});
}
}
const result: NSFWResult = await request.json();
// Process the NSFW analysis result
await handleNSFWResult(result);
return NextResponse.json({success: true});
} catch (error) {
console.error('Webhook processing error:', error);
return NextResponse.json({error: 'Internal server error'}, {status: 500});
}
}
async function handleNSFWResult(result: NSFWResult) {
const {id, nsfw, vision} = result;
// Update database with results
await updateImageAnalysis(id, {
isNsfw: nsfw,
description: vision,
analyzedAt: new Date(),
});
// Trigger any necessary workflows
if (nsfw) {
await handleNSFWContent(id);
} else {
await approveSafeContent(id);
}
}Rate Limiting & Performance
Processing Times
- Standard NSFW Detection: 1-2 seconds
- With Vision Analysis: 2-30 seconds (depends on queue load)
Best Practices
Optimize File Size
Keep images under 500KB when possible for faster processing
Use HTTPS Callbacks
Always use HTTPS for production callback URLs
Implement Retry Logic
Handle temporary service unavailability with exponential backoff
Secure Callback Keys
Use strong, unique API keys for callback authentication
Error Handling
Retry Strategy
async function submitWithRetry(
imageFile: File,
callbackUrl: string,
maxRetries = 3,
): Promise<any> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await checkImageNSFW(imageFile, callbackUrl);
return result;
} catch (error) {
const isLastAttempt = attempt === maxRetries;
if (error.status === 503 && !isLastAttempt) {
// Service unavailable - wait and retry
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
if (error.status >= 400 && error.status < 500) {
// Client error - don't retry
throw error;
}
if (isLastAttempt) {
throw new Error(
`Failed after ${maxRetries} attempts: ${error.message}`,
);
}
}
}
}Common Error Scenarios
| Error | Cause | Solution |
|---|---|---|
| 401 Unauthorized | Invalid API key | Check authorization header |
| 400 Bad Request | File validation failed | Verify file size/format |
| 503 Service Unavailable | All processing servers busy | Implement retry with backoff |
| Callback timeout | Webhook endpoint unreachable | Verify callback URL accessibility |
Need Help? This API is currently in beta. For support, feature requests, or to report issues, please contact our development team.