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

# Anti-AI

> Anti-AI API で画像を AI から保護する

このガイドでは、**Anti-AI 保護注文**を作成する手順を説明します — 画像のアップロードから保護された結果のダウンロードまでです。

このドキュメントのすべてのエンドポイントは純粋な HTTPS と JSON 通信であるため、サーバーだけでなくブラウザからも同じように呼び出せます。各ステップには **React** タブが含まれており、[コピー&ペーストしてそのまま使える完全なコンポーネント](#react-完全なサンプル)はページ下部で確認できます。

## 事前準備

* BIZ MORI API キー（[こちらから発行](https://app.bizmori.com/keys)）
* 保護する画像ファイル（`jpeg`、`jpg`、`png`、`webp`、`tiff`、または `bmp`）

## ステップ1: 注文の作成

注文を作成し、S3 にファイルをアップロードするためのプリサインド URL を受け取ります。

<Tip>
  ファイルをアップロードする代わりに、画像 URL を直接指定することもできます。URL モードの詳細については、[Anti-AI 注文作成 API](/ja/api-reference/anti-ai/create-order) を参照してください。
</Tip>

<CodeGroup>
  ```bash cURL theme={null}
  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": "2f4b6c82-8a6e-4f39-9f8a-7d3b5c1e2a40",
      "files": [{ "fileName": "photo.jpg" }],
      "options": { "strength": "high" }
    }'
  ```

  ```javascript Node.js theme={null}
  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: '2f4b6c82-8a6e-4f39-9f8a-7d3b5c1e2a40',
      files: [{ fileName: 'photo.jpg' }],
      options: { strength: 'high' },
    }),
  });
  const { data } = await response.json();
  ```

  ```jsx React theme={null}
  // すべての JSON 呼び出しで使用する 1 つのヘルパー関数です。API は失敗時に
  // `{ code: "ERROR_CODE" }` を返すため、単純なステータスコードの代わりにこの code をそのまま公開します。
  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` はユーザーが選択した File[] です。
  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 requests

  res = requests.post(
      'https://api.bizmori.com/api/v2/orders/anti-ai',
      headers={'Authorization': 'Bearer YOUR_API_TOKEN'},
      json={
          'idempotencyKey': '2f4b6c82-8a6e-4f39-9f8a-7d3b5c1e2a40',
          'files': [{'fileName': 'photo.jpg'}],
          'options': {'strength': 'high'},
      },
  )
  data = res.json()['data']
  ```
</CodeGroup>

**レスポンス:**

```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"
      }
    ]
  }
}
```

## ステップ2: ファイルのアップロード

ステップ1のレスポンスの `uploadUrl` にファイルを PUT アップロードします。**Authorization ヘッダーは不要です** — S3 への直接アップロードです。

<CodeGroup>
  ```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() はアップロードの進捗を通知できないため、PUT リクエストには XMLHttpRequest を使用します。
  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);
    });
  }

  // レスポンスのファイル一覧はリクエストした順序と同じであるため、`files[index]` は
  // `order.files[index].uploadUrl` に対応します。
  await Promise.all(
    files.map((file, index) =>
      putFile(file, order.files[index].uploadUrl, (ratio) =>
        setProgress((prev) => ({ ...prev, [index]: ratio })),
      ),
    ),
  );
  ```
</CodeGroup>

<Note>
  プリサインド URL は **1時間** 後に失効します。失効した場合は、[URL の再発行](/ja/api-reference/orders/refresh-urls) エンドポイントを使用して新しい URL を取得してください。
</Note>

## ステップ3: 注文の確認

すべてのファイルのアップロードが完了したら、処理を開始するために注文を確認します：

<CodeGroup>
  ```bash cURL theme={null}
  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": "1b7e3c90-6d2a-4f58-a9c1-8e4b7d0f2a63", "orderId": "123456789"}'
  ```

  ```javascript Node.js theme={null}
  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: '1b7e3c90-6d2a-4f58-a9c1-8e4b7d0f2a63',
      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}
  requests.post(
      'https://api.bizmori.com/api/v2/orders/anti-ai/confirm',
      headers={'Authorization': 'Bearer YOUR_API_TOKEN'},
      json={'idempotencyKey': '1b7e3c90-6d2a-4f58-a9c1-8e4b7d0f2a63', 'orderId': '123456789'},
  )
  ```
</CodeGroup>

## ステップ4: 注文ステータスの確認

注文ステータスをポーリングするか、[Webhook](/ja/webhooks) を設定して処理完了の通知を受け取ります。

<CodeGroup>
  ```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));

  // 待機時間が長くなるほどポーリング間隔を広げます：素早い応答は素早く受け取り、時間がかかる
  // 処理では API を過度に呼び出さないようにします。無期限にポーリングせず、10分後には諦めます。
  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']
  ```
</CodeGroup>

**レスポンス:**

```json theme={null}
{
  "data": {
    "type": "antiAi",
    "orderId": "123456789",
    "channel": "api",
    "thumbnailImageUrl": "https://s3.amazonaws.com/...",
    "status": "complete",
    "orderName": "anti_ai_2026-03-18",
    "fileCount": 1,
    "createdAt": "2026-03-18T12:00:00.000Z",
    "updatedAt": "2026-03-18T12:01:30.000Z",
    "errors": null
  }
}
```

注文ステータスの流れ:

| ステータス        | 意味            |
| ------------ | ------------- |
| `pending`    | ファイルのアップロード待ち |
| `inProgress` | 処理中           |
| `complete`   | 完了（ダウンロード可能）  |
| `failed`     | 処理失敗          |

## ステップ5: 結果のダウンロード

ステータスが `complete` になったら、ダウンロード URL を取得します：

<CodeGroup>
  ```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 — 7日間有効
  ```

  ```jsx React theme={null}
  const { url } = await api(`/orders/${order.orderId}/download`);
  // <a href={url} download> 要素に渡してください — この URL は7日間有効です。
  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']
  ```
</CodeGroup>

**レスポンス:**

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

`url` は **7日間** 有効なプリサインド S3 URL です。この URL から保護されたファイルを直接ダウンロードしてください。

Anti-AI API でサポートされているオプションの詳細については、[API リファレンス](/ja/api-reference/anti-ai/create-order)を参照してください。

## React 完全なサンプル

上記すべてを1つのコンポーネントにまとめました：複数ファイルのアップロード、ファイルごとのアップロード進捗、バックオフ付きポーリング、ダウンロードリンクまでを含みます。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 } = {}) {
  // リトライ時も呼び出し元の idempotency key をそのまま使用するため、リトライされた作成
  // リクエストが2番目の注文を作成することはありません。認証失敗は `AUTH_*` コードとして現れ、
  // リトライしても意味がないため、下の throw にそのまま渡されます。
  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 は 2 つの意味を持ちます: 一時的なレート制限と、再試行しても
      // 解消しないプラン使用量の超過（PLAN_LIMIT_EXCEEDED）です。
      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;

    // 選択項目をスナップショットとして固定します。リクエストが進行している間にユーザーがファイルを
    // 変更する可能性があるため、selected[i] は 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` も終了状態です。クリーンアップ処理が 7日後にファイルを削除します。
      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}>
        画像を{files.length}件保護する
      </button>

      {status !== 'idle' && <p>ステータス: {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>
          保護された画像をダウンロード
        </a>
      )}
      {error && <p role="alert">失敗: {error}{orderId && ` (orderId: ${orderId})`}</p>}
    </form>
  );
}
```

<Note>
  `import.meta.env.VITE_MORI_API_TOKEN` は Vite の構文です。Next.js では `process.env.NEXT_PUBLIC_MORI_API_TOKEN` を使用するか、使用中のバンドラーがクライアントコードに公開する方式に従ってください。`crypto.randomUUID()` はセキュアコンテキストでのみ動作します。HTTPS と `localhost` では問題ありませんが、通常の HTTP の LAN アドレスでは使用できません。
</Note>

## エラー処理

| HTTP ステータスコード | 意味                                         | 対応                                                                               |
| ------------- | ------------------------------------------ | -------------------------------------------------------------------------------- |
| `400`         | 不正なリクエスト                                   | パラメータとファイル形式を確認                                                                  |
| `401`         | 認証失敗                                       | API キーを確認                                                                        |
| `429`         | レート制限、またはプラン使用量の超過時は `PLAN_LIMIT_EXCEEDED` | `code` を確認してください。レート制限は再試行で解消しますが、`PLAN_LIMIT_EXCEEDED` は解消しないためプランのアップグレードが必要です |

エラーコードの一覧は、[エラーコード](/ja/errors) ページを参照してください。

## 次のステップ

<CardGroup cols={2}>
  <Card title="不可視 watermark の埋め込み" icon="stamp" href="/ja/quickstart/watermark-embed">
    画像に不可視 watermark を埋め込みます。
  </Card>

  <Card title="不可視 watermark の抽出" icon="magnifying-glass" href="/ja/quickstart/watermark-extract">
    画像から不可視 watermark を検出・抽出します。
  </Card>

  <Card title="AI Detection" icon="robot" href="/ja/quickstart/ai-detection">
    AI 生成画像を確率スコアで検出します。
  </Card>

  <Card title="Webhook" icon="bell" href="/ja/webhooks">
    処理完了時に通知を受け取るために Webhook を設定します。
  </Card>
</CardGroup>
