事前準備
- BIZ MORI API キー(こちらから発行)
- 保護する画像ファイル(
jpeg、jpg、png、webp、tiff、またはbmp)
ステップ1: 注文の作成
注文を作成し、S3 にファイルをアップロードするためのプリサインド URL を受け取ります。ファイルをアップロードする代わりに、画像 URL を直接指定することもできます。URL モードの詳細については、Anti-AI 注文作成 API を参照してください。
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" }
}'
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();
// すべての 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' },
},
});
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']
{
"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 への直接アップロードです。
curl -X PUT "https://s3.amazonaws.com/..." \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpg
// 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 })),
),
),
);
プリサインド URL は 1時間 後に失効します。失効した場合は、URL の再発行 エンドポイントを使用して新しい URL を取得してください。
ステップ3: 注文の確認
すべてのファイルのアップロードが完了したら、処理を開始するために注文を確認します: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"}'
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',
}),
});
await api('/orders/anti-ai/confirm', {
method: 'POST',
idempotencyKey: crypto.randomUUID(),
body: { orderId: order.orderId },
});
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'},
)
ステップ4: 注文ステータスの確認
注文ステータスをポーリングするか、Webhook を設定して処理完了の通知を受け取ります。curl https://api.bizmori.com/api/v2/orders/123456789 \
-H "Authorization: Bearer YOUR_API_TOKEN"
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'
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');
}
res = requests.get(
'https://api.bizmori.com/api/v2/orders/123456789',
headers={'Authorization': 'Bearer YOUR_API_TOKEN'},
)
status = res.json()['data']['status']
{
"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 を取得します:
curl https://api.bizmori.com/api/v2/orders/123456789/download \
-H "Authorization: Bearer YOUR_API_TOKEN"
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日間有効
const { url } = await api(`/orders/${order.orderId}/download`);
// <a href={url} download> 要素に渡してください — この URL は7日間有効です。
setDownloadUrl(url);
res = requests.get(
'https://api.bizmori.com/api/v2/orders/123456789/download',
headers={'Authorization': 'Bearer YOUR_API_TOKEN'},
)
download_url = res.json()['data']['url']
{
"data": {
"url": "https://s3.amazonaws.com/..."
}
}
url は 7日間 有効なプリサインド S3 URL です。この URL から保護されたファイルを直接ダウンロードしてください。
Anti-AI API でサポートされているオプションの詳細については、API リファレンスを参照してください。
React 完全なサンプル
上記すべてを1つのコンポーネントにまとめました:複数ファイルのアップロード、ファイルごとのアップロード進捗、バックオフ付きポーリング、ダウンロードリンクまでを含みます。React 以外の依存関係はありません。AntiAiUploader.jsx
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>
);
}
import.meta.env.VITE_MORI_API_TOKEN は Vite の構文です。Next.js では process.env.NEXT_PUBLIC_MORI_API_TOKEN を使用するか、使用中のバンドラーがクライアントコードに公開する方式に従ってください。crypto.randomUUID() はセキュアコンテキストでのみ動作します。HTTPS と localhost では問題ありませんが、通常の HTTP の LAN アドレスでは使用できません。エラー処理
| HTTP ステータスコード | 意味 | 対応 |
|---|---|---|
400 | 不正なリクエスト | パラメータとファイル形式を確認 |
401 | 認証失敗 | API キーを確認 |
429 | レート制限、またはプラン使用量の超過時は PLAN_LIMIT_EXCEEDED | code を確認してください。レート制限は再試行で解消しますが、PLAN_LIMIT_EXCEEDED は解消しないためプランのアップグレードが必要です |
次のステップ
不可視 watermark の埋め込み
画像に不可視 watermark を埋め込みます。
不可視 watermark の抽出
画像から不可視 watermark を検出・抽出します。
AI Detection
AI 生成画像を確率スコアで検出します。
Webhook
処理完了時に通知を受け取るために Webhook を設定します。