Ext.Systems

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/nsfw

Authentication

The API uses header-based authentication with temporary API keys.

Authorization Header
Authorization: Bearer test_nsfw_xxxxxx

Request Specification

HTTP Method

POST

Content Type

multipart/form-data

Required Parameters

ParameterTypeDescription
imageFileImage file (JPG/PNG, max 1MB)
callback_urlStringHTTPS/HTTP URL to receive results

Optional Parameters

ParameterTypeDefaultDescription
callback_api_keyString-API key for authenticating callback requests
visionBooleanfalseEnable 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)

Successful Submission
{
  "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": "Unauthorized - invalid authorization header"
}
{
  "success": false,
  "message": "File size exceeds 1MB limit. Please upload a smaller file."
}
{
  "success": false,
  "message": "Failed to process image analysis request"
}
{
  "success": false,
  "message": "Image analysis service temporarily unavailable"
}

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:

Callback Request Headers
Content-Type: application/json
Authorization: Bearer your_callback_api_key

Callback Payload

Basic NSFW Detection Result
{
  "id": "OrOcqNpFd7oE_84n8oNzT",
  "nsfw": false
}
NSFW Detection with Vision Analysis
{
  "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."
}
Error During Processing
{
  "id": "OrOcqNpFd7oE_84n8oNzT",
  "nsfw": false,
  "vision": "Error: Could not analyze image using vision tools"
}

Callback Response Fields

FieldTypeDescription
idStringUnique job identifier from original request
nsfwBooleantrue if NSFW content detected, false otherwise
visionStringDetailed image description (only if vision: true was requested)

Usage Examples

cURL Example

Basic NSFW Check
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/nsfw

JavaScript/Fetch Example

Frontend Upload with Vision
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

Server-side Processing
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

Python Implementation
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

Symfony Service Implementation
<?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 {}
Symfony Controller Usage
<?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);
    }
}
services.yaml Configuration
# config/services.yaml
services:
  App\Service\NSFWDetectionService:
    arguments:
      $httpClient: '@http_client'
      $apiKey: '%env(NSFW_API_KEY)%'
Environment Configuration
# .env
NSFW_API_KEY=test_nsfw_xxxxxx
CALLBACK_API_KEY=your_secure_callback_key

Callback Handler Examples

Express.js Webhook Handler

Express.js Callback 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

Next.js Webhook 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

Error Handling

Retry Strategy

Retry Logic Implementation
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

ErrorCauseSolution
401 UnauthorizedInvalid API keyCheck authorization header
400 Bad RequestFile validation failedVerify file size/format
503 Service UnavailableAll processing servers busyImplement retry with backoff
Callback timeoutWebhook endpoint unreachableVerify callback URL accessibility

Need Help? This API is currently in beta. For support, feature requests, or to report issues, please contact our development team.

On this page