The Copyleaks AI Image Detection API is a powerful tool to determine if a given image was generated or partially generated by an AI. The API is synchronous, meaning you get the results in the same API call. This guide will walk you through the process of submitting an image to the Copyleaks AI Detector using multipart/form-data format and understanding the results.

Get started

1

Before you begin

Before you start, ensure you have the following:
2

Installation

Choose your preferred method for making API calls.
# macOS
brew install curl

# Ubuntu/Debian
sudo apt-get install curl

# Windows - download from https://curl.se
pip install copyleaks
npm install plagiarism-checker
Install-Package Copyleaks
composer require copyleaks/php-plagiarism-checker
gem install plagiarism-checker
<!-- Add to your pom.xml (requires Java 11+) -->
<dependency>
    <groupId>com.copyleaks.sdk</groupId>
    <artifactId>copyleaks-java-sdk</artifactId>
    <version>5.1.0</version>
</dependency>
HTTP needs no installation - call the API with any standard HTTP client, or import our Postman collection for a quicker start.
3

Login

To perform a scan, we first need to generate an access token. For that, we will use the login endpoint. The API key can be found on the Copyleaks API Dashboard.Upon successful authentication, you will receive a token that must be attached to subsequent API calls via the Authorization: Bearer <TOKEN> header. This token remains valid for 48 hours.
POST https://id.copyleaks.com/v3/account/login/api

Headers
Content-Type: application/json

Body
{
    "email": "[email protected]",
    "key": "00000000-0000-0000-0000-000000000000"
}
export COPYLEAKS_EMAIL="[email protected]"
export COPYLEAKS_API_KEY="your-api-key-here"

curl --request POST \
  --url https://id.copyleaks.com/v3/account/login/api \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --data "{
    \"email\": \"${COPYLEAKS_EMAIL}\",
    \"key\": \"${COPYLEAKS_API_KEY}\"
  }"
from copyleaks.copyleaks import Copyleaks

EMAIL_ADDRESS = "[email protected]"
API_KEY = "your-api-key-here"

# Login to Copyleaks
auth_token = Copyleaks.login(EMAIL_ADDRESS, API_KEY)
print("Logged successfully!\nToken:", auth_token)
const { Copyleaks } = require("plagiarism-checker");

const EMAIL_ADDRESS = "[email protected]";
const API_KEY = "your-api-key-here";
const copyleaks = new Copyleaks();

// Login function
function loginToCopyleaks() {
  return copyleaks.loginAsync(EMAIL_ADDRESS, API_KEY).then(
    (loginResult) => {
      console.log("Login successful!");
      console.log("Access Token:", loginResult.access_token);
      return loginResult;
    },
    (err) => {
      console.error('Login failed:', err);
      throw err;
    }
  );
}

loginToCopyleaks();
import classes.Copyleaks;
import models.response.CopyleaksAuthToken;

String EMAIL_ADDRESS = "[email protected]";
String API_KEY = "00000000-0000-0000-0000-000000000000";

// Login to Copyleaks
try {
    CopyleaksAuthToken authToken = Copyleaks.login(EMAIL_ADDRESS, API_KEY);
    System.out.println("Logged in successfully!");
} catch (Exception e) {
    System.out.println("Failed to login: " + e.getMessage());
    System.exit(1);
}
Response
{
    "access_token": "<ACCESS_TOKEN>",
    ".issued": "2025-07-31T10:19:40.0690015Z",
    ".expires": "2025-08-02T10:19:40.0690016Z"
}
Save this token. It is valid for 48 hours and can be reused for subsequent API calls.
4

Submit for analysis

Use the AI Image Detector Endpoint to send an image for analysis. We suggest you provide a unique scanId for each submission.This guide uses multipart/form-data format, which sends the image file directly without base64 encoding overhead.
Use multipart/form-data to send a binary image file. Use application/json when you need to submit image data as a base64-encoded string. Note that multipart only accepts binary files, base64-encoded images are not supported for multipart requests.

Image Requirements

  • Size: Minimum 512×512px, maximum 6000×4500px (27 megapixels)
  • File size: Less than 32MB
  • Formats: PNG, JPG, JPEG, BMP, WebP, HEIC/HEIF
For testing, set "sandbox": true. Sandbox mode is free and returns mock results.
POST https://api.copyleaks.com/v1/ai-image-detector/my-image-scan-1/check

Headers
Authorization: Bearer <YOUR_AUTH_TOKEN>
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

Body
------WebKitFormBoundary
Content-Disposition: form-data; name="image"; filename="test-image.png"
Content-Type: image/png

[binary image data]
------WebKitFormBoundary
Content-Disposition: form-data; name="filename"

test-image.png
------WebKitFormBoundary
Content-Disposition: form-data; name="sandbox"

true
------WebKitFormBoundary
Content-Disposition: form-data; name="model"

ai-image-1-ultra
------WebKitFormBoundary--
curl -X POST "https://api.copyleaks.com/v1/ai-image-detector/my-image-scan-1/check" \
     -H "Authorization: Bearer <YOUR_AUTH_TOKEN>" \
     -F "image=@/path/to/test-image.png" \
     -F "filename=test-image.png" \
     -F "sandbox=true" \
     -F "model=ai-image-1-ultra"
import requests

# Prepare the request
url = 'https://api.copyleaks.com/v1/ai-image-detector/my-image-scan-1/check'
headers = {
    'Authorization': 'Bearer YOUR_LOGIN_TOKEN'
}

# Prepare multipart form data
with open('test-image.png', 'rb') as image_file:
    files = {
        'image': ('test-image.png', image_file, 'image/png')
    }
    data = {
        'filename': 'test-image.png',
        'sandbox': 'true',
        'model': 'ai-image-1-ultra'
    }
    
    # Send the request
    response = requests.post(url, files=files, data=data, headers=headers)

result = response.json()
print(f"AI Detection Summary: {result['summary']}")
const imageFile = document.getElementById('fileInput').files[0];
const formData = new FormData();

formData.append('image', imageFile);
formData.append('filename', imageFile.name);
formData.append('sandbox', 'true');
formData.append('model', 'ai-image-1-ultra');

const response = await fetch('https://api.copyleaks.com/v1/ai-image-detector/my-image-scan-1/check', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_LOGIN_TOKEN'
  },
  body: formData
});

const result = await response.json();
console.log('AI Detection Result:', result);
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class AiImageDetectionMultipart {
    public static void main(String[] args) throws IOException, InterruptedException {
        String authToken = "YOUR_LOGIN_TOKEN";
        String imagePath = "path/to/your/test-image.png";
        String scanId = "my-java-scan-1";
        String boundary = "----WebKitFormBoundary" + System.currentTimeMillis();

        Path path = Paths.get(imagePath);
        byte[] imageBytes = Files.readAllBytes(path);
        String filename = "test-image.png";

        // Build multipart body
        List<byte[]> byteArrays = new ArrayList<>();
        
        // Image field
        String imagePart = "--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"image\"; filename=\"" + filename + "\"\r\n" +
            "Content-Type: image/png\r\n\r\n";
        byteArrays.add(imagePart.getBytes());
        byteArrays.add(imageBytes);
        byteArrays.add("\r\n".getBytes());
        
        // Other fields
        String otherFields = "--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"filename\"\r\n\r\n" +
            filename + "\r\n" +
            "--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"sandbox\"\r\n\r\n" +
            "true\r\n" +
            "--" + boundary + "\r\n" +
            "Content-Disposition: form-data; name=\"model\"\r\n\r\n" +
            "ai-image-1-ultra\r\n" +
            "--" + boundary + "--\r\n";
        byteArrays.add(otherFields.getBytes());
        
        // Combine all parts
        byte[] multipartBody = combineByteArrays(byteArrays);

        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.copyleaks.com/v1/ai-image-detector/" + scanId + "/check"))
                .header("Authorization", "Bearer " + authToken)
                .header("Content-Type", "multipart/form-data; boundary=" + boundary)
                .POST(HttpRequest.BodyPublishers.ofByteArray(multipartBody))
                .build();

        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println("Status Code: " + response.statusCode());
        System.out.println("Response Body: " + response.body());
    }
    
    private static byte[] combineByteArrays(List<byte[]> arrays) {
        int totalLength = arrays.stream().mapToInt(a -> a.length).sum();
        byte[] result = new byte[totalLength];
        int offset = 0;
        for (byte[] array : arrays) {
            System.arraycopy(array, 0, result, offset, array.length);
            offset += array.length;
        }
        return result;
    }
}
5

Interpreting the response

The API response contains:
  • A summary object with the overall percentage of AI vs. human pixels
  • A result object with a Run-Length Encoded (RLE) mask
  • imageInfo with the image dimensions and metadata (when available)
  • scannedDocument with scan details including credits used

Understanding the RLE Mask

Run-Length Encoding (RLE) is a compression method used to represent the AI-detected regions of the image efficiently. It provides an array of starts positions and lengths for each run of AI-detected pixels in a flattened 1D version of the image.You can decode this RLE data to create a binary mask. Below are implementations in different languages:
def decode_mask(rle_data, image_width, image_height):
    """
    Decode RLE mask data into a binary mask array.
    
    Args:
        rle_data (dict): Dictionary with 'starts' and 'lengths' arrays
        image_width (int): Width of the image in pixels
        image_height (int): Height of the image in pixels
        
    Returns:
        list: A 1D array where 1 represents AI-detected pixels
    """
    total_pixels = image_width * image_height
    mask = [0] * total_pixels
    
    starts = rle_data.get('starts', [])
    lengths = rle_data.get('lengths', [])
    
    for i in range(len(starts)):
        start = starts[i]
        length = lengths[i]
        
        for j in range(length):
            position = start + j
            if position < total_pixels:
                mask[position] = 1
                
    return mask
    
# Example usage:
# result = response.json()
# binary_mask = decode_mask(
#     result['result'], 
#     result['imageInfo']['shape']['width'], 
#     result['imageInfo']['shape']['height']
# )
function decodeMask(rleData, imageWidth, imageHeight) {
  const totalPixels = imageWidth * imageHeight;
  const mask = new Array(totalPixels).fill(0);

  const starts = rleData.starts || [];
  const lengths = rleData.lengths || [];

  for (let i = 0; i < starts.length; i++) {
    const start = starts[i];
    const length = lengths[i];

    for (let j = 0; j < length; j++) {
      const position = start + j;
      if (position < totalPixels) {
        mask[position] = 1;
      }
    }
  }
  return mask;
}

// Example usage:
// const { result, imageInfo } = await response.json();
// const binaryMask = decodeMask(result, imageInfo.shape.width, imageInfo.shape.height);
/**
 * Decodes RLE mask data into a binary mask array
 * 
 * @param rleMask The RLE mask with starts and lengths arrays
 * @param width Image width in pixels
 * @param height Image height in pixels
 * @return Binary mask where true represents AI-detected pixels
 */
public static boolean[] decodeMask(RleMask rleMask, int width, int height) {
    int totalPixels = width * height;
    boolean[] mask = new boolean[totalPixels];
    
    if (rleMask == null || rleMask.starts() == null || rleMask.lengths() == null) {
        return mask;
    }
    
    for (int i = 0; i < rleMask.starts().length; i++) {
        int start = rleMask.starts()[i];
        int length = rleMask.lengths()[i];
        
        for (int j = 0; j < length; j++) {
            int position = start + j;
            if (position < totalPixels) {
                mask[position] = true;
            }
        }
    }
    
    return mask;
}

// Example usage:
// Response contains: { "result": { "starts": [0, 512...], "lengths": [256, 512...] }, "imageInfo": {...} }
// boolean[] binaryMask = decodeMask(
//     new RleMask(result.getJSONObject("result").getJSONArray("starts"), result.getJSONObject("result").getJSONArray("lengths")), 
//     result.getJSONObject("imageInfo").getJSONObject("shape").getInt("width"),
//     result.getJSONObject("imageInfo").getJSONObject("shape").getInt("height")
// );
The resulting binary mask is an array where a 1 (or true in Java) represents an AI-detected pixel. You can use this mask to create a visual overlay on the original image.

Creating a Visual Overlay

After decoding the RLE data, you can use the resulting mask to draw a semi-transparent overlay on the original image. Here are some examples of how to achieve this:
# Requires: pip install Pillow
from PIL import Image
import numpy as np
def apply_overlay(image_path, mask_array, output_path):
    """
    Apply a red (1) and green (0) overlay to the image and save the result.
    Args:
        image_path (str): Path to the original image
        mask_array (np.ndarray): 2D numpy array with 1 (red) and 0 (green)
        output_path (str): Path to save the output image
    """
    height, width = mask_array.shape
    original_img = Image.open(image_path).convert('RGBA')
    overlay = Image.new('RGBA', (width, height), (0, 0, 0, 0))
    overlay_pixels = overlay.load()
    for y in range(height):
        for x in range(width):
            if mask_array[y, x] == 1:
                overlay_pixels[x, y] = (255, 0, 0, 120)  # Red, semi-transparent
            else:
                overlay_pixels[x, y] = (0, 255, 0, 120)  # Green, semi-transparent
    result_img = Image.alpha_composite(original_img, overlay)
    result_img.save(output_path)
# Usage example:
width = result['imageInfo']['shape']['width']
height = result['imageInfo']['shape']['height']
mask_array = np.array(binary_mask, dtype=np.uint8).reshape((height, width))
apply_overlay('test-image.png', mask_array, 'output-with-overlay.png')
// Assumes 'decodeMask' function from above is available
/**
 * Creates a canvas with the original image and an overlay showing AI vs human regions
 * @param {HTMLImageElement} imageElement - The image element to overlay
 * @param {Object} rleData - The RLE mask data with starts and lengths arrays
 * @returns {HTMLCanvasElement} Canvas with the original image and overlay
 */
function createOverlay(imageElement, rleData) {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const width = imageElement.width;
  const height = imageElement.height;
  canvas.width = width;
  canvas.height = height;

  // Draw original image
  ctx.drawImage(imageElement, 0, 0);
  
  // Get the binary mask
  const binaryMask = decodeMask(rleData, width, height);
  
  // Create an ImageData object to manipulate pixels directly
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
  
  // Apply overlay for each pixel
  for (let i = 0; i < binaryMask.length; i++) {
    const pixelIndex = i * 4; // RGBA data has 4 values per pixel
    
    if (binaryMask[i] === 1) {
      // AI-generated area (red overlay)
      data[pixelIndex] = data[pixelIndex] * 0.7 + 255 * 0.3; // R
      data[pixelIndex + 1] = data[pixelIndex + 1] * 0.7;     // G
      data[pixelIndex + 2] = data[pixelIndex + 2] * 0.7;     // B
      data[pixelIndex + 3] = 255;                           // A
    } else {
      // Human-generated area (green overlay)
      data[pixelIndex] = data[pixelIndex] * 0.7;             // R
      data[pixelIndex + 1] = data[pixelIndex + 1] * 0.7 + 255 * 0.3; // G
      data[pixelIndex + 2] = data[pixelIndex + 2] * 0.7;     // B
      data[pixelIndex + 3] = 255;                           // A
    }
  }
  
  // Put the modified image data back on the canvas
  ctx.putImageData(imageData, 0, 0);
  return canvas;
}

// Example usage:
function displayImageWithOverlay(imagePath, apiResult) {
  // Create image element
  const img = new Image();
  img.crossOrigin = "Anonymous";
  
  // When image loads, create and display the overlay
  img.onload = () => {
    // Get image dimensions from the API result
    const width = apiResult.imageInfo.shape.width;
    const height = apiResult.imageInfo.shape.height;
    
    // Create the overlay canvas
    const canvas = createOverlay(img, apiResult.result);
    
    // Add to page and optionally download
    document.body.appendChild(canvas);
    
    // Optionally: convert to a downloadable image
    canvas.toBlob(blob => {
      const link = document.createElement('a');
      link.download = 'overlay-image.png';
      link.href = URL.createObjectURL(blob);
      link.textContent = 'Download Image with Overlay';
      document.body.appendChild(link);
    });
  };
  
  // Set the image source to load it
  img.src = imagePath;
}
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import javax.imageio.ImageIO;

// First, create a record for the RLE data:
// For Java 16+:
// public record RleMask(int[] starts, int[] lengths) {}
// For earlier Java versions:
public static class RleMask {
    private final int[] starts;
    private final int[] lengths;
    
    public RleMask(int[] starts, int[] lengths) {
        this.starts = starts;
        this.lengths = lengths;
    }
    
    public int[] starts() { return starts; }
    public int[] lengths() { return lengths; }
}

/**
 * Class to handle applying AI detection overlays to images
 */
public class ImageOverlay {
    /**
     * Applies red (AI) and green (human) overlays to the image based on mask data
     * 
     * @param imagePath Path to the original image file
     * @param maskArray 2D boolean array where true represents AI-detected pixels
     * @param outputPath Path to save the output image
     * @return The overlaid image as a BufferedImage
     */
    public static BufferedImage applyOverlay(String imagePath, boolean[][] maskArray, String outputPath) 
            throws IOException {
        // Read the original image
        BufferedImage original = ImageIO.read(new File(imagePath));
        int width = original.getWidth();
        int height = original.getHeight();
        
        // Create overlay image with transparent background
        BufferedImage overlay = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = overlay.createGraphics();
        
        // Apply overlay for each pixel
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (y < maskArray.length && x < maskArray[0].length) {
                    if (maskArray[y][x]) {
                        // AI-detected area - red overlay
                        g.setColor(new Color(255, 0, 0, 120)); // Red with 47% opacity
                    } else {
                        // Human-created area - green overlay
                        g.setColor(new Color(0, 255, 0, 120)); // Green with 47% opacity
                    }
                    g.fillRect(x, y, 1, 1);
                }
            }
        }
        g.dispose();
        
        // Combine original and overlay
        BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2 = result.createGraphics();
        g2.drawImage(original, 0, 0, null);
        g2.drawImage(overlay, 0, 0, null);
        g2.dispose();
        
        // Save the resulting image
        ImageIO.write(result, "PNG", new File(outputPath));
        
        return result;
    }
    
    /**
     * Convert 1D mask array to 2D for easier processing
     * 
     * @param mask 1D boolean array representing the mask
     * @param width Image width
     * @param height Image height
     * @return 2D boolean array
     */
    public static boolean[][] convertTo2DMask(boolean[] mask, int width, int height) {
        boolean[][] result = new boolean[height][width];
        for (int i = 0; i < mask.length; i++) {
            int y = i / width;
            int x = i % width;
            if (y < height && x < width) {
                result[y][x] = mask[i];
            }
        }
        return result;
    }
}

// Example usage:
public static void main(String[] args) throws IOException {
    // Step 1: Get the API result with RLE data (simplified example)
    int[] starts = {0, 512, 1536, 2560};
    int[] lengths = {256, 512, 768, 1024};
    RleMask rleMask = new RleMask(starts, lengths);
    
    // Step 2: Decode the RLE mask
    int width = 1024;
    int height = 768;
    boolean[] binaryMask = decodeMask(rleMask, width, height);
    
    // Step 3: Convert to 2D array for easier processing
    boolean[][] mask2D = ImageOverlay.convertTo2DMask(binaryMask, width, height);
    
    // Step 4: Apply overlay and save
    String imagePath = "path/to/your/image.jpg";
    String outputPath = "output-with-overlay.png";
    ImageOverlay.applyOverlay(imagePath, mask2D, outputPath);
    
    System.out.println("Overlay applied and saved to " + outputPath);
}
For a complete breakdown of all fields in the response, see the AI Image Detection Response documentation.
6

Summary

You have successfully submitted an image for AI detection. You can now use the JSON response in your application to take further action based on the findings.

Frequently asked questions

Yes. You send the image to the check endpoint and receive the results in the same API call, with no webhook required.
Images must be between 512×512px and 6000×4500px (27 megapixels) and under 32MB. Supported formats are PNG, JPG, JPEG, BMP, WebP, and HEIC/HEIF.
Yes. Use application/json to send a base64-encoded image. The multipart/form-data method shown in this guide accepts only binary files, not base64.
A summary with the overall percentage of AI vs. human pixels, a result object with a Run-Length Encoded (RLE) mask of AI-detected regions, imageInfo with dimensions and metadata, and scannedDocument with scan details. See the AI Image Detection Response reference.
Decode the RLE mask into a binary mask (one value per pixel), then draw a semi-transparent overlay on the original image. This guide includes ready-to-use decode and overlay code in Python, JavaScript, and Java.

Next steps

API Reference

Explore the full API reference for the AI Image Detection endpoint.

AI Image Detector Response

Explore the full response for the AI Image Detection.

Performance Best Practices

Learn optimization strategies for performance and accuracy with image detection.

Testing Methodology

Learn about the accuracy and testing methodology of the AI Image Detection product.