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

# 不可視 watermark の埋め込み

> 画像に不可視 watermark を埋め込む

このガイドでは、画像に不可視 watermark を埋め込む手順を説明します — 注文の作成から、watermark が埋め込まれた結果のダウンロードまでです。

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

## 事前準備

* BIZ MORI API キー（[こちらから発行](https://app.bizmori.com/keys)）
* 不可視 watermark を埋め込むファイル（`jpeg`、`jpg`、`png`、`webp`、`tiff`、`bmp`、または `pdf`）

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

watermark テキストを指定して、不可視 watermark 埋め込み注文を作成します。

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.bizmori.com/api/v2/orders/wtr-embed \
    -H "Authorization: Bearer YOUR_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "idempotencyKey": "2f4b6c82-8a6e-4f39-9f8a-7d3b5c1e2a40",
      "files": [
        {
          "fileName": "photo.jpg",
          "watermarks": [{ "text": "MORI_WATERMARK" }]
        }
      ]
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.bizmori.com/api/v2/orders/wtr-embed', {
    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',
          watermarks: [{ text: 'MORI_WATERMARK' }],
        },
      ],
    }),
  });
  const { data } = await response.json();
  ```

  ```jsx React theme={null}
  // JSON を使用するすべての呼び出しで使うヘルパー関数です。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;
  }

  // `file` はユーザーが選択した単一の File、`watermarkTexts` は string[] です
  const order = await api('/orders/wtr-embed', {
    method: 'POST',
    idempotencyKey: crypto.randomUUID(),
    body: {
      files: [
        {
          fileName: file.name,
          watermarks: watermarkTexts.map((text) => ({ text })),
        },
      ],
    },
  });
  ```

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

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

**レスポンス:**

```json theme={null}
{
  "data": {
    "orderName": "wtr_embed_2026-03-18",
    "orderId": "123456789",
    "status": "pending",
    "files": [
      {
        "fileId": 1,
        "fileName": "photo.jpg",
        "uploadUrl": "https://s3.amazonaws.com/...",
        "fileKey": "wtr-embed/123456789/images/1/photo.jpg",
        "fileFormat": "JPG",
        "fileType": "IMG"
      }
    ]
  }
}
```

## ステップ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);
    });
  }

  // ファイルはちょうど1つなので、アップロード進捗は単一の数値で表されます。
  await putFile(file, order.files[0].uploadUrl, (ratio) => setProgress(ratio));
  ```
</CodeGroup>

<Info>
  **別途、確認（confirm）ステップは必要ありません。** Anti-AI や AI Detection サービスとは異なり、不可視 watermark の埋め込みはファイルのアップロードが完了すると自動的に処理が開始されます。アップロード後はそのまま注文ステータスを確認してください。
</Info>

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

## ステップ3: 注文ステータスの確認と結果のダウンロード

注文ステータスをポーリングするか、[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": "watermarkEmbed",
    "orderId": "123456789",
    "channel": "api",
    "thumbnailImageUrl": "https://s3.amazonaws.com/...",
    "status": "complete",
    "orderName": "wtr_embed_2026-03-18",
    "fileCount": 1,
    "createdAt": "2026-03-18T12:00:00.000Z",
    "updatedAt": "2026-03-18T12:01:30.000Z",
    "errors": null,
    "watermarks": ["MORI_WATERMARK"]
  }
}
```

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

ステータスが `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 です。

不可視 watermark 埋め込み API でサポートされているオプションの詳細については、[API リファレンス](/ja/api-reference/watermark-embed/create-order)を参照してください。

## React 完全なサンプル

上記すべてを1つのコンポーネントにまとめました：進捗を表示する単一ファイルのアップロード、追加・削除と重複チェックが可能な watermark テキストのリスト、バックオフ付きポーリング、ダウンロードリンクまでを含みます。React 以外の依存関係はありません。

```jsx WatermarkEmbedUploader.jsx theme={null}
import { useMemo, 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,.pdf';
const MAX_WATERMARKS = 10;
const MAX_TEXT_LENGTH = 1000;

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 WatermarkEmbedUploader() {
  const [file, setFile] = useState(null);
  // 遅延初期化：こうしないと crypto.randomUUID() がレンダリングのたびに実行されます。
  const [watermarks, setWatermarks] = useState(() => [{ id: crypto.randomUUID(), text: '' }]);
  const [progress, setProgress] = useState(0);
  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';

  // 最初に出現した後に現れる同じテキストをすべて重複としてマークします。空の値は
  // 例外です — 下の「空テキスト禁止」ルールで既に除外されているためです。
  const duplicateIds = useMemo(() => {
    const seen = new Map();
    const duplicates = new Set();
    watermarks.forEach((watermark) => {
      const text = watermark.text.trim();
      if (text === '') return;
      if (seen.has(text)) duplicates.add(watermark.id);
      else seen.set(text, watermark.id);
    });
    return duplicates;
  }, [watermarks]);

  const canSubmit =
    file !== null &&
    watermarks.length > 0 &&
    watermarks.every((watermark) => watermark.text.trim() !== '') &&
    duplicateIds.size === 0 &&
    !busy;

  function updateWatermark(id, text) {
    setWatermarks((prev) => prev.map((watermark) => (watermark.id === id ? { ...watermark, text } : watermark)));
  }

  function addWatermark() {
    if (watermarks.length >= MAX_WATERMARKS) return;
    setWatermarks((prev) => [...prev, { id: crypto.randomUUID(), text: '' }]);
  }

  function removeWatermark(id) {
    setWatermarks((prev) => prev.filter((watermark) => watermark.id !== id));
  }

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

    // 選択された値をスナップショットとして保存します。リクエストが進行している間もユーザーはフォームを
    // 編集できるため、アップロードには送信時点の値をそのまま使用する必要があります。
    const selectedFile = file;
    const selectedWatermarks = watermarks;
    setError(null);
    setOrderId(null);
    setDownloadUrl(null);
    setProgress(0);

    try {
      setStatus('creating order');
      const order = await api('/orders/wtr-embed', {
        method: 'POST',
        idempotencyKey: crypto.randomUUID(),
        body: {
          files: [
            {
              fileName: selectedFile.name,
              watermarks: selectedWatermarks.map((watermark) => ({ text: watermark.text.trim() })),
            },
          ],
        },
      });

      setOrderId(order.orderId);
      setStatus('uploading');
      await putFile(selectedFile, order.files[0].uploadUrl, setProgress);

      // 別途、確認（confirm）ステップはありません — アップロードが完了すると処理が自動的に開始されます。
      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={embedWatermarks}>
      <input
        type="file"
        accept={ACCEPT_FORMATS}
        disabled={busy}
        onChange={(event) => {
          setFile(event.target.files[0] ?? null);
          setError(null);
          setProgress(0);
          setDownloadUrl(null);
        }}
      />

      <ul>
        {watermarks.map((watermark, index) => (
          <li key={watermark.id}>
            <input
              type="text"
              value={watermark.text}
              maxLength={MAX_TEXT_LENGTH}
              disabled={busy}
              placeholder={`watermark テキスト #${index + 1}`}
              onChange={(event) => updateWatermark(watermark.id, event.target.value)}
              style={{ borderColor: duplicateIds.has(watermark.id) ? 'red' : undefined }}
            />
            <button type="button" disabled={busy || watermarks.length === 1} onClick={() => removeWatermark(watermark.id)}>
              削除
            </button>
          </li>
        ))}
      </ul>
      <button type="button" disabled={busy || watermarks.length >= MAX_WATERMARKS} onClick={addWatermark}>
        watermark テキストを追加 ({watermarks.length}/{MAX_WATERMARKS})
      </button>

      <button type="submit" disabled={!canSubmit}>
        watermark を埋め込む
      </button>

      {status !== 'idle' && <p>ステータス: {status}</p>}
      {status === 'uploading' && <p>アップロード進捗: {Math.round(progress * 100)}%</p>}

      {downloadUrl && (
        <a href={downloadUrl} download>
          watermark 付き画像をダウンロード
        </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="Anti-AI" icon="shield" href="/ja/quickstart/anti-ai">
    画像を AI 学習および生成から保護します。
  </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>
