> ## Documentation Index
> Fetch the complete documentation index at: https://docs.contentcare.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# AI Detection

> Detect AI-generated images with probability scoring

This guide walks you through detecting whether an image is AI-generated — from uploading an image to reading the detection result.

Every endpoint here is plain HTTPS and JSON, so the same calls work from a server or straight from the browser. Each step includes a **React** tab, and a [full copy-paste component](#full-react-example) is at the bottom of the page.

## Prerequisites

* A BIZ MORI API key ([get one here](https://app.contentcare.ai/keys))
* An image file to analyze (`jpeg`, `jpg`, `png`, `webp`, `bmp`, or `tiff`)

> [!NOTE]
>
> Use the automatically issued [`sk_test_` key](https://docs.contentcare.ai/test-api-keys) to exercise the order lifecycle without running AI Detection or consuming credits. Test uploads discard file contents and return deterministic probability values. Use a live API key when you need an actual detection result.

## Step 1: Create an order

Create an AI Detection order with your image file name.

```bash cURL theme={null}
ORDER_IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]')
curl -X POST https://api.bizmori.com/api/v2/orders/ai-detection \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotencyKey": "'$ORDER_IDEMPOTENCY_KEY'",
    "fileName": "photo.jpg"
  }'
```

```javascript Node.js theme={null}
import { randomUUID } from 'node:crypto';

const orderIdempotencyKey = randomUUID();
const response = await fetch('https://api.bizmori.com/api/v2/orders/ai-detection', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    idempotencyKey: orderIdempotencyKey,
    fileName: 'photo.jpg',
  }),
});
const { data } = await response.json();
```

```jsx React theme={null}
// One helper for every JSON call. The API returns `{ code: "ERROR_CODE" }`
// on failure, so surface that code instead of a bare status number.
const API_BASE = 'https://api.bizmori.com/api/v2';
const API_TOKEN = import.meta.env.VITE_MORI_API_TOKEN;

async function api(path, { method = 'GET', body, idempotencyKey } = {}) {
  const res = await fetch(`${API_BASE}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${API_TOKEN}`,
      ...(body && { 'Content-Type': 'application/json' }),
    },
    body: body && JSON.stringify({ idempotencyKey, ...body }),
  });

  if (!res.ok) {
    const { code } = await res.json().catch(() => ({}));
    throw new Error(code ?? `HTTP_${res.status}`);
  }
  return (await res.json()).data;
}

// `file` is the single File the user picked
const order = await api('/orders/ai-detection', {
  method: 'POST',
  idempotencyKey: crypto.randomUUID(),
  body: { fileName: file.name },
});
```

```python Python theme={null}
import uuid
import requests

order_idempotency_key = str(uuid.uuid4())
res = requests.post(
    'https://api.bizmori.com/api/v2/orders/ai-detection',
    headers={'Authorization': 'Bearer YOUR_API_TOKEN'},
    json={
        'idempotencyKey': order_idempotency_key,
        'fileName': 'photo.jpg',
    },
)
data = res.json()['data']
```

**Response:**

```json theme={null}
{
  "data": {
    "orderId": "123456789",
    "orderName": "ai_detection_2026-03-18",
    "status": "pending",
    "file": {
      "fileId": 1,
      "fileName": "photo.jpg",
      "uploadUrl": "https://s3.amazonaws.com/...",
      "fileKey": "ai-detection/123456789/1/photo.jpg"
    }
  }
}
```

> [!TIP]
>
> You can optionally request heatmap and overlay images by adding `"options": { "generateHeatmap": true, "generateOverlay": true }` to the request body. These will be available in the order detail once processing completes.

## Step 2: Upload the file

PUT your file to the presigned `uploadUrl` from Step 1. This is a direct S3 upload — **no Authorization header needed**.

```bash cURL theme={null}
curl -X PUT "https://s3.amazonaws.com/..." \
  -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg
```

```jsx React theme={null}
// fetch() cannot report upload progress, so use XMLHttpRequest for the PUT.
function putFile(file, uploadUrl, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('PUT', uploadUrl);
    xhr.setRequestHeader('Content-Type', file.type);

    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) onProgress(event.loaded / event.total);
    };
    xhr.onload = () =>
      xhr.status < 300 ? resolve() : reject(new Error(`UPLOAD_FAILED_${xhr.status}`));
    xhr.onerror = () => reject(new Error('UPLOAD_NETWORK_ERROR'));

    xhr.send(file);
  });
}

// The order response holds a single `file`, not a `files` array.
await putFile(file, order.file.uploadUrl, (ratio) => setProgress(ratio));
```

> [!NOTE]
>
> Live keys receive S3 presigned URLs. Test keys receive `https://api.bizmori.com/api/v2/test-uploads/{signedToken}`; both expire after **1 hour** and accept the same unauthenticated PUT. Test uploads are streamed only, not stored or processed. Use [Refresh URLs](https://docs.contentcare.ai/api-reference/orders/refresh-urls.md) if a URL expires.

## Step 3: Confirm the order

After uploading, call confirm to start detection processing:

```bash cURL theme={null}
CONFIRM_IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]')
curl -X POST https://api.bizmori.com/api/v2/orders/ai-detection/confirm \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"idempotencyKey\": \"$CONFIRM_IDEMPOTENCY_KEY\", \"orderId\": \"123456789\"}"
```

```javascript Node.js theme={null}
const confirmIdempotencyKey = randomUUID();
await fetch('https://api.bizmori.com/api/v2/orders/ai-detection/confirm', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    idempotencyKey: confirmIdempotencyKey,
    orderId: '123456789',
  }),
});
```

```jsx React theme={null}
await api('/orders/ai-detection/confirm', {
  method: 'POST',
  idempotencyKey: crypto.randomUUID(),
  body: { orderId: order.orderId },
});
```

```python Python theme={null}
confirm_idempotency_key = str(uuid.uuid4())
requests.post(
    'https://api.bizmori.com/api/v2/orders/ai-detection/confirm',
    headers={'Authorization': 'Bearer YOUR_API_TOKEN'},
    json={'idempotencyKey': confirm_idempotency_key, 'orderId': '123456789'},
)
```

## Step 4: Check the result

> [!NOTE]
>
> This step uses `GET /api/v2/orders/{orderId}`, which is deprecated but unchanged. For new integrations, use [Get order details (v3)](https://docs.contentcare.ai/api-reference/orders/get-order-v3.md). It returns the outcome in `result.verdict` and `result.probability` instead of the top-level `statusCode` and `probability`.

Poll the order or use [webhooks](https://docs.contentcare.ai/webhooks.md) to receive a push notification when detection completes. The detection result is included directly in the order detail — there is no separate download step.

```bash cURL theme={null}
curl https://api.bizmori.com/api/v2/orders/123456789 \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

```javascript Node.js theme={null}
const res = await fetch('https://api.bizmori.com/api/v2/orders/123456789', {
  headers: { 'Authorization': 'Bearer YOUR_API_TOKEN' },
});
const { data } = await res.json();
// data.probability: 0-1
// data.statusCode: 'likely_ai' | 'uncertain_ai' | 'uncertain_real' | 'likely_real'
```

```jsx React theme={null}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Back off as the wait grows: quick answers stay quick, long jobs stop
// hammering the API. Give up after 10 minutes rather than polling forever.
async function pollOrder(orderId, { onStatus, timeoutMs = 10 * 60 * 1000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  let delay = 2000;

  while (Date.now() < deadline) {
    const order = await api(`/orders/${orderId}`);
    onStatus?.(order.status);
    if (['complete', 'failed', 'expired'].includes(order.status)) return order;

    await sleep(delay);
    delay = Math.min(delay * 1.5, 15000);
  }
  throw new Error('ORDER_POLL_TIMEOUT');
}
```

```python Python theme={null}
res = requests.get(
    'https://api.bizmori.com/api/v2/orders/123456789',
    headers={'Authorization': 'Bearer YOUR_API_TOKEN'},
)
data = res.json()['data']
probability = data['probability']
status_code = data['statusCode']
```

**Response:**

```json theme={null}
{
  "data": {
    "type": "aiDetection",
    "orderId": "123456789",
    "status": "complete",
    "probability": 0.92,
    "statusCode": "likely_ai",
    "heatmapUrl": "https://s3.amazonaws.com/...",
    "overlayUrl": "https://s3.amazonaws.com/..."
  }
}
```

### Reading the result

The `probability` field (0–1) indicates how likely the image is AI-generated. The `statusCode` provides a human-readable interpretation:

| `statusCode` | Probability range | Meaning |
| - | - | - |
| `likely_real` | \< 0.25 | Highly likely human-created |
| `uncertain_real` | 0.25 – 0.5 | Likely human-created |
| `uncertain_ai` | 0.5 – 0.75 | Likely AI-generated |
| `likely_ai` | ≥ 0.75 | Highly likely AI-generated |

> [!NOTE]
>
> `heatmapUrl` and `overlayUrl` are only present if you requested them via `options.generateHeatmap` and `options.generateOverlay` in Step 1.
> The result images are usually WebP files resized so that the longer side is at most 1600 px. `originalImageUrl` is a resized copy of your input in the same way, and `thumbnailImageUrl` is a thumbnail up to 740×400. If the resized images are not ready, you get the original files instead.

## Full React example

Everything above, wired into one component: image upload with progress, backoff polling, and the detection result. No dependencies beyond React.

```jsx AiDetectionChecker.jsx theme={null}
import { useRef, useState } from 'react';

const API_BASE = 'https://api.bizmori.com/api/v2';
const API_TOKEN = import.meta.env.VITE_MORI_API_TOKEN;

const ACCEPT_FORMATS = '.jpg,.jpeg,.png,.webp,.tiff,.bmp';

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function api(path, { method = 'GET', body, idempotencyKey } = {}) {
  // Retries reuse the caller's idempotency key, so a retried create can never
  // produce a second order. Auth failures surface as `AUTH_*` codes — retrying
  // those will not help, so they fall through to the throw below.
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(`${API_BASE}${path}`, {
      method,
      headers: {
        Authorization: `Bearer ${API_TOKEN}`,
        ...(body && { 'Content-Type': 'application/json' }),
      },
      body: body && JSON.stringify({ idempotencyKey, ...body }),
    });

    if (!res.ok) {
      const { code } = await res.json().catch(() => ({}));
      // 429 covers two cases: a transient rate limit, and PLAN_LIMIT_EXCEEDED,
      // which means the plan quota is spent and will never clear on retry.
      if (res.status === 429 && code !== 'PLAN_LIMIT_EXCEEDED' && attempt < 3) {
        await sleep(2 ** attempt * 1000);
        continue;
      }
      throw new Error(code ?? `HTTP_${res.status}`);
    }
    return (await res.json()).data;
  }
}

function putFile(file, uploadUrl, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('PUT', uploadUrl);
    xhr.setRequestHeader('Content-Type', file.type);

    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) onProgress(event.loaded / event.total);
    };
    xhr.onload = () =>
      xhr.status < 300 ? resolve() : reject(new Error(`UPLOAD_FAILED_${xhr.status}`));
    xhr.onerror = () => reject(new Error('UPLOAD_NETWORK_ERROR'));

    xhr.send(file);
  });
}


async function pollOrder(orderId, { onStatus, timeoutMs = 10 * 60 * 1000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  let delay = 2000;

  while (Date.now() < deadline) {
    const order = await api(`/orders/${orderId}`);
    onStatus?.(order.status);
    if (['complete', 'failed', 'expired'].includes(order.status)) return order;

    await sleep(delay);
    delay = Math.min(delay * 1.5, 15000);
  }
  throw new Error('ORDER_POLL_TIMEOUT');
}

export default function AiDetectionChecker() {
  const [file, setFile] = useState(null);
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState('idle');
  const [result, setResult] = useState(null);
  const [error, setError] = useState(null);
  const [orderId, setOrderId] = useState(null);
  const inFlight = useRef(false);

  const busy = status !== 'idle' && status !== 'complete';

  async function detectImage(event) {
    event.preventDefault();
    if (inFlight.current || !file) return;
    inFlight.current = true;

    // Snapshot the selection. The user can swap the file while requests are
    // in flight, and `selected` has to stay paired with the order it created.
    const selected = file;
    setError(null);
    setOrderId(null);
    setResult(null);
    setProgress(0);

    try {
      setStatus('creating order');
      const order = await api('/orders/ai-detection', {
        method: 'POST',
        idempotencyKey: crypto.randomUUID(),
        body: {
          fileName: selected.name,
          options: { generateHeatmap: true, generateOverlay: true },
        },
      });

      setOrderId(order.orderId);
      setStatus('uploading');
      await putFile(selected, order.file.uploadUrl, setProgress);

      setStatus('confirming');
      await api('/orders/ai-detection/confirm', {
        method: 'POST',
        idempotencyKey: crypto.randomUUID(),
        body: { orderId: order.orderId },
      });

      const finished = await pollOrder(order.orderId, { onStatus: setStatus });
      // `expired` is terminal too: the cleanup job drops the files after 7 days.
      if (finished.status !== 'complete') throw new Error(`ORDER_${finished.status.toUpperCase()}`);

      setResult(finished);
      setStatus('complete');
    } catch (caught) {
      setError(caught.message);
      setStatus('idle');
    } finally {
      inFlight.current = false;
    }
  }

  return (
    <form onSubmit={detectImage}>
      <input
        type="file"
        accept={ACCEPT_FORMATS}
        disabled={busy}
        onChange={(event) => {
          setFile(event.target.files[0] ?? null);
          setError(null);
          setProgress(0);
          setResult(null);
        }}
      />
      <button type="submit" disabled={busy || !file}>
        Detect image
      </button>

      {status !== 'idle' && <p>Status: {status}</p>}
      {status === 'uploading' && <p>Upload progress: {Math.round(progress * 100)}%</p>}

      {result && (
        <div>
          <p>
            Probability: {Math.round(result.probability * 100)}% — {result.statusCode}
          </p>
          {result.heatmapUrl && <img src={result.heatmapUrl} alt="AI detection heatmap" />}
          {result.overlayUrl && <img src={result.overlayUrl} alt="AI detection overlay" />}
        </div>
      )}
      {error && <p role="alert">Failed: {error}{orderId && ` (orderId: ${orderId})`}</p>}
    </form>
  );
}
```

> [!NOTE]
>
> `import.meta.env.VITE_MORI_API_TOKEN` is Vite's syntax. Use `process.env.NEXT_PUBLIC_MORI_API_TOKEN` on Next.js, or whatever your bundler exposes to client code. `crypto.randomUUID()` needs a secure context: HTTPS and `localhost` are fine, a plain-HTTP LAN address is not.

## Error handling

| HTTP Status | Meaning | Action |
| - | - | - |
| `400` | Invalid request | Check parameters and file format |
| `401` | Authentication failed | Verify your API key |
| `429` | Rate limit, or `PLAN_LIMIT_EXCEEDED` when the plan quota is spent | Check the `code` field. A rate limit clears on retry; `PLAN_LIMIT_EXCEEDED` does not — upgrade the plan |

For the complete error code reference, see [Error Codes](https://docs.contentcare.ai/errors.md).

## Next steps

> **[Anti-AI](https://docs.contentcare.ai/quickstart/anti-ai.md)**
>
> Protect images from AI training and generation.

> **[Watermark Embed](https://docs.contentcare.ai/quickstart/watermark-embed.md)**
>
> Embed invisible watermarks into images.

> **[Watermark Extract](https://docs.contentcare.ai/quickstart/watermark-extract.md)**
>
> Detect and extract watermarks from images.

> **[Webhooks](https://docs.contentcare.ai/webhooks.md)**
>
> Set up webhooks to get notified when processing completes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.