> ## 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.

# Anti-AI

> Protect your images from AI with the Anti-AI API

This guide walks you through creating an **Anti-AI protection order** — from uploading an image to downloading the protected 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.

> [!TIP]
>
> You can also provide image URLs directly instead of uploading files. See the [Create Anti-AI order API](https://docs.contentcare.ai/api-reference/anti-ai/create-order.md) for URL mode details.

## Prerequisites

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

> [!NOTE]
>
> Use the automatically issued [`sk_test_` key](https://docs.contentcare.ai/test-api-keys) to exercise the order lifecycle without running Anti-AI processing or consuming credits. Test uploads discard file contents and do not produce a downloadable result. Use a live API key when you need an actual protected image.

## Step 1: Create an order

Create an order and receive presigned S3 URLs for uploading your files.

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

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

const orderIdempotencyKey = randomUUID();
const response = await fetch('https://api.bizmori.com/api/v2/orders/anti-ai', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    idempotencyKey: orderIdempotencyKey,
    files: [{ fileName: 'photo.jpg' }],
    options: { strength: 'high' },
  }),
});
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;
}

// `files` is the File[] the user picked
const order = await api('/orders/anti-ai', {
  method: 'POST',
  idempotencyKey: crypto.randomUUID(),
  body: {
    files: files.map((file) => ({ fileName: file.name })),
    options: { strength: 'high' },
  },
});
```

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

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

**Response:**

```json theme={null}
{
  "data": {
    "orderName": "anti_ai_2026-02-19",
    "orderId": "123456789",
    "status": "pending",
    "files": [
      {
        "fileId": 1,
        "fileName": "photo.jpg",
        "uploadUrl": "https://s3.amazonaws.com/...",
        "fileKey": "temp/123456789/0/photo.jpg"
      }
    ]
  }
}
```

## Step 2: Upload files

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 response lists files in the order you sent them, so `files[index]`
// belongs to `order.files[index].uploadUrl`.
await Promise.all(
  files.map((file, index) =>
    putFile(file, order.files[index].uploadUrl, (ratio) =>
      setProgress((prev) => ({ ...prev, [index]: 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 all files, call confirm to start processing:

```bash cURL theme={null}
CONFIRM_IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]')
curl -X POST https://api.bizmori.com/api/v2/orders/anti-ai/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/anti-ai/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/anti-ai/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/anti-ai/confirm',
    headers={'Authorization': 'Bearer YOUR_API_TOKEN'},
    json={'idempotencyKey': confirm_idempotency_key, 'orderId': '123456789'},
)
```

## Step 4: Check order status

> [!NOTE]
>
> Steps 4 and 5 use `GET /api/v2/orders/{orderId}` and `GET /api/v2/orders/{orderId}/download`, which are deprecated but unchanged. For new integrations, use [Get order details (v3)](https://docs.contentcare.ai/api-reference/orders/get-order-v3.md) and [Get order result download URLs (v3)](https://docs.contentcare.ai/api-reference/orders/download-order-v3.md). They return every order type in one shape, and the v3 download returns a URL for each output file plus a ZIP archive of all results.

Poll the order or use [webhooks](https://docs.contentcare.ai/webhooks.md) to receive a push notification when processing completes.

```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.status: 'pending' | 'inProgress' | 'complete' | 'failed'
```

```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'},
)
status = res.json()['data']['status']
```

Order statuses:

| Status | Meaning |
| - | - |
| `pending` | Waiting for file upload |
| `inProgress` | Processing |
| `complete` | Ready for download |
| `failed` | Processing failed |

## Step 5: Download the result

Once `status` is `complete`, fetch the download URL:

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

```javascript Node.js theme={null}
const res = await fetch('https://api.bizmori.com/api/v2/orders/123456789/download', {
  headers: { 'Authorization': 'Bearer YOUR_API_TOKEN' },
});
const { data } = await res.json();
// data.url — valid for 7 days
```

```jsx React theme={null}
const { url } = await api(`/orders/${order.orderId}/download`);
// Hand it to an <a href={url} download> — the URL is valid for 7 days
setDownloadUrl(url);
```

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

**Response:**

```json theme={null}
{
  "data": {
    "url": "https://s3.amazonaws.com/..."
  }
}
```

The `url` is a presigned S3 URL valid for **7 days**. Download the protected file directly from this URL.

## Full React example

Everything above, wired into one component: multiple files, per-file upload progress, backoff polling, and a download link. No dependencies beyond React.

```jsx AntiAiUploader.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 MAX_FILES = 100;

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 AntiAiUploader() {
  const [files, setFiles] = useState([]);
  const [progress, setProgress] = useState({});
  const [status, setStatus] = useState('idle');
  const [downloadUrl, setDownloadUrl] = useState(null);
  const [error, setError] = useState(null);
  const [orderId, setOrderId] = useState(null);
  const inFlight = useRef(false);

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

  async function protectImages(event) {
    event.preventDefault();
    if (inFlight.current || files.length === 0) return;
    inFlight.current = true;

    // Snapshot the selection. The user can swap files while requests are in
    // flight, and selected[i] has to stay paired with uploadUrls[i].
    const selected = files;
    setError(null);
    setOrderId(null);
    setDownloadUrl(null);
    setProgress({});

    try {
      setStatus('creating order');
      const order = await api('/orders/anti-ai', {
        method: 'POST',
        idempotencyKey: crypto.randomUUID(),
        body: {
          files: selected.map((file) => ({ fileName: file.name })),
          options: { strength: 'high' },
        },
      });

      setOrderId(order.orderId);
      setStatus('uploading');
      await Promise.all(
        selected.map((file, index) =>
          putFile(file, order.files[index].uploadUrl, (ratio) =>
            setProgress((prev) => ({ ...prev, [index]: ratio })),
          ),
        ),
      );

      setStatus('confirming');
      await api('/orders/anti-ai/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()}`);

      setStatus('fetching download URL');
      const { url } = await api(`/orders/${order.orderId}/download`);
      setDownloadUrl(url);
      setStatus('complete');
    } catch (caught) {
      setError(caught.message);
      setStatus('idle');
    } finally {
      inFlight.current = false;
    }
  }

  return (
    <form onSubmit={protectImages}>
      <input
        type="file"
        multiple
        accept={ACCEPT_FORMATS}
        disabled={busy}
        onChange={(event) => {
          setFiles([...event.target.files].slice(0, MAX_FILES));
          setDownloadUrl(null);
          setError(null);
          setProgress({});
        }}
      />
      <button type="submit" disabled={busy || files.length === 0}>
        Protect {files.length} image{files.length === 1 ? '' : 's'}
      </button>

      {status !== 'idle' && <p>Status: {status}</p>}

      <ul>
        {files.map((file, index) => (
          <li key={`${file.name}-${index}`}>
            {file.name} — {Math.round((progress[index] ?? 0) * 100)}%
          </li>
        ))}
      </ul>

      {downloadUrl && (
        <a href={downloadUrl} download>
          Download protected images
        </a>
      )}
      {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.

> [!NOTE]
>
> Test orders do not produce a result file: their `downloadUrl` is `null`, and the download endpoint can return `PROCESSED_FILE_NOT_FOUND`.

## 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

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

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

> **[AI Detection](https://docs.contentcare.ai/quickstart/ai-detection.md)**
>
> Detect AI-generated images with probability scores.

> **[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.