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

# Webhook

> 注文が完了または失敗した際にリアルタイムでプッシュ通知を受け取る

Webhookは、注文処理が完了または失敗した際にサーバーへ HTTP `POST` コールバックを送信します。[注文の取得](https://docs.contentcare.ai/ja/api-reference/orders/get-order-v3.md) エンドポイントを繰り返しポーリングする代わりにWebhookを使用すると、結果が準備でき次第すぐに通知を受け取ることができます。

## Webhookの設定

**1. Webhookエンドポイントの登録**

APIまたは [ContentCare ダッシュボード](https://app.contentcare.ai/webhooks) からWebhookエンドポイントを作成できます。APIを使って作成するには、[Webhookの作成](https://docs.contentcare.ai/ja/api-reference/webhooks/create-webhook.md) エンドポイントを使用します:

```bash theme={null}
curl -X POST https://api.bizmori.com/api/v2/orders/webhooks \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "マイWebhook",
    "url": "https://your-server.com/webhook"
  }'
```

**レスポンス:**

```json theme={null}
{
  "data": {
    "id": 1,
    "name": "マイWebhook",
    "secret": "whsec_xxxxxxxxxxxxxxxxxx"
  }
}
```

> [!WARNING]
>
> `secret` はすぐに保存してください — **一度だけ表示され**、再度取得することはできません。受信するすべてのWebhook署名の検証に必要です。

テスト注文のイベントを受信するには、`isTest: true` のテスト Webhook エンドポイントを作成します。テスト API キーは同じ所有者のテストエンドポイントだけを管理できます。本番モードと別所有者のリソースは公開されません。詳しくは [テスト API キー](https://docs.contentcare.ai/ja/test-api-keys.md) を参照してください。

**2. エンドポイントの実装**

Webhookハンドラーは次を実行する必要があります:

* JSON本文で `POST` リクエストを受け付ける
* **5秒以内**に `2xx` ステータスコードで応答する
* 処理前に `X-MoriBiz-Signature` ヘッダーを検証する

**3. 署名の検証**

すべてのリクエストには `X-MoriBiz-Signature` ヘッダーが含まれます。使用している言語での実装については、以下の [署名検証](#署名検証) を参照してください。

## イベントタイプ

| イベントタイプ | 説明 |
| - | - |
| `order.antiAi.completed` | Anti-AI処理が正常に完了しました |
| `order.antiAi.failed` | Anti-AI処理が失敗しました |
| `order.watermarkEmbed.completed` | watermark 埋め込みが正常に完了しました |
| `order.watermarkEmbed.failed` | watermark 埋め込みが失敗しました |
| `order.watermarkExtract.completed` | watermark 抽出が正常に完了しました |
| `order.watermarkExtract.failed` | watermark 抽出が失敗しました |
| `order.aiDetection.completed` | AI Detectionが正常に完了しました |
| `order.aiDetection.failed` | AI Detectionが失敗しました |

## Webhookペイロード

すべてのWebhookペイロードは次の構造に従います:

```json theme={null}
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "eventType": "order.antiAi.completed",
  "occurredAt": "2026-02-19T12:00:00.000Z",
  "data": {
    "orderId": "123456789",
    "orderName": "anti_ai_2026-02-19",
    "createdAt": "2026-02-19T11:50:00.000Z",
    "updatedAt": "2026-02-19T12:00:00.000Z",
    "completedAt": "2026-02-19T12:00:00.000Z",
    "status": "complete"
    // ...各機能ごとの追加フィールド
  }
}
```

### 完了イベント — 共通フィールド

| フィールド | 型 | 説明 |
| - | - | - |
| `orderId` | string | 注文ID |
| `orderName` | string | 注文名 |
| `createdAt` | string | 注文作成日時（ISO 8601） |
| `updatedAt` | string | 注文の最終更新日時（ISO 8601） |
| `completedAt` | string | 処理完了日時（ISO 8601） |
| `status` | string | 常に `complete` |

### 失敗イベント — 共通フィールド

| フィールド | 型 | 説明 |
| - | - | - |
| `orderId` | string | 注文ID |
| `orderName` | string | 注文名 |
| `createdAt` | string | 注文作成日時（ISO 8601） |
| `updatedAt` | string | 注文の最終更新日時（ISO 8601） |
| `failedAt` | string | 処理失敗日時（ISO 8601） |
| `status` | string | 常に `failed` |
| `errorCode` | string | エラーコード。[失敗イベントのエラーコード](#失敗イベントのエラーコード)を参照してください |
| `errorMessage` | string | エラーコードに対応する固定の案内文（韓国語）。処理サーバーの原文は含みません |

### 失敗イベントのエラーコード

`errorCode` は常に以下の公開コードのいずれかです。内部処理サーバーのコードやエラー原文は含みません。

| サービス | `errorCode` | 意味 |
| - | - | - |
| Anti-AI | `PROCESSING_ERROR` | 処理に失敗しました |
| watermark 埋め込み / 抽出 | `WATERMARK_FILE_FORMAT_UNSUPPORTED` | 対応していないファイル形式（アニメーション画像、内容と形式が一致しないファイルを含む）。JPEG、PNG、WEBP、BMP、TIFF の静止画像または PDF を使用してください |
| watermark 埋め込み / 抽出 | `WATERMARK_FILE_CORRUPTED` | ファイルが破損しているか読み取れません |
| watermark 埋め込み / 抽出 | `WATERMARK_FILE_SIZE_UNSUPPORTED` | 空のファイル、またはサイズ上限を超えています |
| watermark 埋め込み / 抽出 | `WATERMARK_IMAGE_COLOR_MODE_UNSUPPORTED` | パレット、CMYK、16 ビット、グレースケール＋透過など対応していないカラーモード。RGB、グレースケール、または透過付き RGBA（PNG、WEBP）画像を使用してください |
| watermark 埋め込み / 抽出 | `WATERMARK_IMAGE_DIMENSIONS_UNSUPPORTED` | 対応していない画像の解像度または向き |
| watermark 埋め込み / 抽出 | `WATERMARK_PDF_UNSUPPORTED` | 処理できない PDF（暗号化、ページ数超過、破損など） |
| watermark 埋め込み / 抽出 | `WATERMARK_INPUT_UNSUPPORTED` | その他、watermark エンジンが処理できない入力（不透明な領域がない、小さすぎる、または偏っている透過背景の画像を含む） |
| watermark 埋め込み / 抽出 | `WATERMARK_ENGINE_UNAVAILABLE` | watermark エンジンの一時的なエラー。同じファイルで再試行してください |
| watermark 埋め込み / 抽出 | `ORDER_TIMEOUT` | 処理がタイムアウトしました。再試行してください |
| watermark 埋め込み / 抽出 | `UPLOAD_EXPIRED` | アップロード待機時間内にファイルがアップロードされませんでした。新しい注文を作成してください |
| watermark 埋め込み / 抽出 | `WATERMARK_V2_API_SUNSET` | v2 のサポート終了（2026 年 10 月 9 日 09:00 KST）までに処理が始まっていなかった v2 注文。課金されず、クレジットは戻ります。v3 で新しい注文を作成してください |
| watermark 埋め込み / 抽出 | `PROCESSING_ERROR` | その他の処理失敗 |
| AI Detection | `AI_DETECTION_INVALID_IMAGE` | 画像を読み取れません |
| AI Detection | `AI_DETECTION_TIMEOUT` | 判定がタイムアウトしました。再試行してください |
| AI Detection | `AI_DETECTION_SERVICE_ERROR` | 判定サービスの一時的なエラー。再試行してください |
| AI Detection | `AI_DETECTION_FAILED` | その他の判定失敗 |

テスト注文は常に `PROCESSING_ERROR` です。新しいコードが追加されても連携が壊れないよう、不明なコードは一般的な失敗として処理してください。

### Anti-AI / watermark 埋め込み — 追加フィールド（完了）

| フィールド | 型 | 説明 |
| - | - | - |
| `fileCount` | integer | 処理されたファイル数 |
| `downloadUrl` | string \| null | 本番注文では 7 日間有効な結果ファイルのダウンロードURL。テスト注文では `null` |

### watermark 抽出 — 追加フィールド（完了）

`status` フィールドは常に `complete` です。watermark が検出されたかどうかは `statusCode` で確認してください。

| フィールド | 型 | 説明 |
| - | - | - |
| `statusCode` | string | `detected` または `undetected` |
| `watermarkFound` | boolean | watermark が検出されたかどうか |
| `watermarkInfo.text` | string | 検出された watermark のテキスト（`watermarkFound` が `true` の場合のみ） |

ペイロードではテキストのみを公開し、MID、`midDecimal`、`midHex` は含みません。

### AI Detection — 追加フィールド（完了）

| フィールド | 型 | 説明 |
| - | - | - |
| `probability` | number | AI生成確率（0.0～1.0） |
| `heatmapUrl` | string \| null | AI判定ヒートマップ画像のダウンロードURL（`generateHeatmap` オプション使用時のみ返却） |
| `overlayUrl` | string \| null | 元画像上にヒートマップを重ねたオーバーレイ画像のダウンロードURL（`generateOverlay` オプション使用時のみ返却） |

`heatmapUrl` と `overlayUrl` は、長辺を1600px以内に縮小したWebP画像を指します。注文が完了した時点でリサイズ画像が用意できていない場合は、元のサイズのPNGファイルを指します。Webhookのペイロードは注文完了時に確定するため、注文詳細では後からリサイズ画像が返されることがあります。

## 署名検証

`X-MoriBiz-Signature` ヘッダーを確認してWebhookの真正性を検証します。イベントを処理する**前に**必ず検証してください。

```javascript Node.js theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody) // 生のリクエストボディ文字列を使用
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express ハンドラー
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-moribiz-signature'];

  if (!verifyWebhookSignature(req.body.toString(), signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const { eventType, data } = JSON.parse(req.body);
  console.log(`注文 ${data.orderId} の ${eventType} イベントを受信`);
  res.sendStatus(200);
});
```

```python Python theme={null}
import hmac
import hashlib

def verify_webhook_signature(raw_body: str, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        raw_body.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

# Flask ハンドラー
from flask import Flask, request, abort
import json, os

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-MoriBiz-Signature')
    raw_body = request.get_data(as_text=True)

    if not verify_webhook_signature(raw_body, signature, os.environ['WEBHOOK_SECRET']):
        abort(401)

    event = json.loads(raw_body)
    return '', 200
```

```php PHP theme={null}
<?php
function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool {
    $expected = hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signature);
}

$signature = $_SERVER['HTTP_X_MORIBIZ_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');

if (!verifyWebhookSignature($rawBody, $signature, getenv('WEBHOOK_SECRET'))) {
    http_response_code(401);
    exit('Unauthorized');
}

$event = json_decode($rawBody, true);
http_response_code(200);
```

```ruby Ruby theme={null}
require 'openssl'
require 'json'

def verify_webhook_signature(raw_body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body)
  Rack::Utils.secure_compare(expected, signature)
end

# Sinatra / Rails
post '/webhook' do
  raw_body = request.body.read
  signature = request.env['HTTP_X_MORIBIZ_SIGNATURE']

  halt 401, 'Unauthorized' unless
    verify_webhook_signature(raw_body, signature, ENV['WEBHOOK_SECRET'])

  event = JSON.parse(raw_body)
  status 200
end
```

```go Go theme={null}
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"net/http"
	"os"
)

func verifySignature(rawBody []byte, signature, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	signature := r.Header.Get("X-MoriBiz-Signature")

	if !verifySignature(body, signature, os.Getenv("WEBHOOK_SECRET")) {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}
	w.WriteHeader(http.StatusOK)
}
```

```java Java theme={null}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.HexFormat;

public static boolean verifySignature(String rawBody, String signature, String secret)
        throws Exception {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
    String expected = HexFormat.of().formatHex(mac.doFinal(rawBody.getBytes()));
    return MessageDigest.isEqual(expected.getBytes(), signature.getBytes());
}

// Spring Boot ハンドラー
@PostMapping("/webhook")
public ResponseEntity<Void> handleWebhook(
        @RequestBody String rawBody,
        @RequestHeader("X-MoriBiz-Signature") String signature) throws Exception {

    if (!verifySignature(rawBody, signature, webhookSecret)) {
        return ResponseEntity.status(401).build();
    }
    return ResponseEntity.ok().build();
}
```

## 再送ポリシー

BIZ MORIは、信頼性の高いWebhook配信を保証するために多段階の再送システムを使用しています。

### 初期再送

エンドポイントが `2xx` ステータスコードを返さない場合、BIZ MORIは直ちに再送を試みます:

| 試行 | 遅延 |
| - | - |
| 1回目の再送 | 1秒 |
| 2回目の再送 | 2秒 |
| 3回目の再送 | 4秒 |

### スケジューラーベースの再送

初期再送がすべて失敗すると、イベントは間隔が徐々に広がるスケジューラーベースの再送キューに登録されます:

| 試行 | 遅延 |
| - | - |
| 4回目の再送 | 30分 |
| 5回目の再送 | 2時間 |
| 6回目の再送 | 4時間 |

スケジューラーベースの再送がすべて尽きると、イベントは `FAILED` としてマークされます。[Webhookイベント一覧](https://docs.contentcare.ai/ja/api-reference/webhooks/list-events.md) エンドポイントを使用して、失敗したイベントを確認できます。

### 手動再送API

自動再送プロセスとは関係なく、再送APIを使用していつでも手動でWebhookを再送できます:

* **単一イベントの再送**: [Webhookイベントの再送](https://docs.contentcare.ai/ja/api-reference/webhooks/retry-event.md) エンドポイントを使用して、特定の失敗したイベントを再試行します。
* **一括再送**: [失敗したWebhookイベントの一括再送](https://docs.contentcare.ai/ja/api-reference/webhooks/retry-failed-events.md) エンドポイントを使用して、特定の期間（最大7日間）内に失敗したすべてのイベントを再試行します。

```bash theme={null}
# 単一イベントの再送
curl -X POST https://api.bizmori.com/api/v2/orders/webhooks/{webhookId}/events/{eventId}/retry \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# 特定期間内に失敗したイベントの一括再送
curl -X POST https://api.bizmori.com/api/v2/orders/webhooks/{webhookId}/events/retry-failed \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fromDate": "2026-01-01",
    "toDate": "2026-01-07"
  }'
```

### 失敗通知メール

初回のWebhook配信に失敗すると、アカウント所有者にWebhook配信失敗の通知メールが送信されます。過剰な通知を防ぐため、このメールは**1日最大1回**のみ送信されます。

## Webhookの管理

| 操作 | エンドポイント |
| - | - |
| Webhook一覧の取得 | [GET /webhooks](https://docs.contentcare.ai/ja/api-reference/webhooks/list-webhooks.md) |
| Webhookの作成 | [POST /webhooks](https://docs.contentcare.ai/ja/api-reference/webhooks/create-webhook.md) |
| Webhook詳細の取得 | [GET /webhooks/{id}](https://docs.contentcare.ai/ja/api-reference/webhooks/get-webhook.md) |
| Webhookの更新 | [PUT /webhooks/{id}](https://docs.contentcare.ai/ja/api-reference/webhooks/update-webhook.md) |
| Webhookの削除 | [DELETE /webhooks/{id}](https://docs.contentcare.ai/ja/api-reference/webhooks/delete-webhook.md) |
| 配信イベントの確認 | [GET /webhooks/{id}/events](https://docs.contentcare.ai/ja/api-reference/webhooks/list-events.md) |
| 単一イベントの再送 | [POST /webhooks/{id}/events/{eventId}/retry](https://docs.contentcare.ai/ja/api-reference/webhooks/retry-event.md) |
| 失敗イベントの一括再送 | [POST /webhooks/{id}/events/retry-failed](https://docs.contentcare.ai/ja/api-reference/webhooks/retry-failed-events.md) |


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