Skip to content

Webhook

概要

Webhookを使用すると、抽出が完了または失敗した際にリアルタイムの通知を受け取ることができます。APIをポーリングする代わりに、DocMapがステータスの変更後すぐにエンドポイントにPOSTリクエストを送信し、抽出結果を含めます。

基本情報:

  • アカウントあたり最大 10個のアクティブなWebhook
  • ペイロードはHMAC-SHA256で署名されるため、真正性を検証できます
  • 配信タイムアウトはWebhookあたり 10秒
  • 配信はファイアアンドフォーゲット方式 -- 1つのWebhookの失敗が他のWebhookをブロックすることはありません

Webhookの作成

ダッシュボードから

  1. DocMapダッシュボードの Webhook に移動します
  2. Webhookを作成 をクリックします
  3. エンドポイントURLを入力します(本番環境ではHTTPSが必須)
  4. 購読するイベントを選択します
  5. 署名シークレットをすぐにコピーしてください -- 再度表示されることはありません

APIから

bash
curl -X POST https://api.docmap.io/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/docmap",
    "events": ["extraction.completed", "extraction.failed"]
  }'
typescript
const response = await fetch("https://api.docmap.io/v1/webhooks", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://your-app.com/webhooks/docmap",
    events: ["extraction.completed", "extraction.failed"],
  }),
});

const { data } = await response.json();

// すぐに保存してください -- 再度表示されることはありません
console.log("Signing Secret:", data.secret);
console.log("Webhook ID:", data.webhook.id);
python
import requests

response = requests.post(
    "https://api.docmap.io/v1/webhooks",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    },
    json={
        "url": "https://your-app.com/webhooks/docmap",
        "events": ["extraction.completed", "extraction.failed"],
    },
)

data = response.json()["data"]

# すぐに保存してください -- 再度表示されることはありません
print("Signing Secret:", data["secret"])
print("Webhook ID:", data["webhook"]["id"])

WARNING

署名シークレットはWebhookの作成時に 一度だけ 返されます。環境変数またはシークレットマネージャーに安全に保存してください。

ペイロード形式

イベントが発生すると、DocMapは以下の構造でWebhook URLにPOSTリクエストを送信します:

json
{
  "event": "extraction.completed",
  "data": {
    "id": "extract-abc123",
    "userId": "uid_xyz789",
    "templateId": "tpl_inv001",
    "templateName": "Standard Invoice",
    "fileName": "invoice-march.pdf",
    "status": "completed",
    "extractedData": {
      "vendor_name": "Acme Corp",
      "invoice_number": "INV-2024-003",
      "total_amount": 4750.00
    },
    "error": null,
    "variables": [...],
    "source": "api",
    "runId": null,
    "processingTimeMs": 3842,
    "createdAt": "2024-11-20T14:30:00.000Z"
  },
  "timestamp": "2024-11-20T14:30:04.000Z"
}

ヘッダー

各Webhookリクエストには以下のヘッダーが含まれます:

ヘッダー説明
Content-Typeapplication/json
X-DocMap-SignatureHMAC-SHA256署名: sha256={hex_digest}
X-DocMap-Eventイベント名(例:extraction.completed

署名の検証

リクエストがDocMapからのものであることを確認するために、X-DocMap-Signature ヘッダーを必ず検証してください。署名は、署名シークレットをキーとして使用した、生のリクエストボディのHMAC-SHA256ハッシュです。

typescript
import { createHmac, timingSafeEqual } from 'node:crypto'

function verifyWebhookSignature(
  body: string,
  signature: string,
  secret: string,
): boolean {
  const expected = createHmac('sha256', secret)
    .update(body)
    .digest('hex')

  const received = signature.replace('sha256=', '')

  return timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(received, 'hex'),
  )
}

// Expressの例
app.post('/webhooks/docmap', (req, res) => {
  const signature = req.headers['x-docmap-signature'] as string
  const body = JSON.stringify(req.body)

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

  const { event, data } = req.body

  console.log(`Received ${event} for extraction ${data.id}`)

  // Webhookを処理...

  res.status(200).send('OK')
})
python
import hmac
import hashlib

def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode('utf-8'),
        body,
        hashlib.sha256,
    ).hexdigest()

    received = signature.replace('sha256=', '')

    return hmac.compare_digest(expected, received)

# Flaskの例
from flask import Flask, request

app = Flask(__name__)

@app.post('/webhooks/docmap')
def handle_webhook():
    signature = request.headers.get('X-DocMap-Signature', '')
    body = request.get_data()

    if not verify_webhook_signature(body, signature, WEBHOOK_SECRET):
        return 'Invalid signature', 401

    payload = request.get_json()
    event = payload['event']
    data = payload['data']

    print(f"Received {event} for extraction {data['id']}")

    # Webhookを処理...

    return 'OK', 200

TIP

タイミング攻撃を防ぐために、必ず定数時間比較(timingSafeEqualhmac.compare_digest など)を使用してください。

イベントリファレンス

イベントトリガー
extraction.completed抽出が正常に完了しました。data.status"completed" で、data.extractedData に結果が含まれます。
extraction.failed抽出が失敗しました。data.status"failed" で、data.error にエラーメッセージが含まれます。

ベストプラクティス

  • HTTPSを使用する。 Webhook URLには常にHTTPSエンドポイントを使用してください。HTTPエンドポイントは本番環境で拒否される場合があります。

  • 署名を検証する。 Webhookを処理する前に、必ず X-DocMap-Signature ヘッダーを検証してください。これにより、リクエストがDocMapからのものであり、改ざんされていないことを確認できます。

  • 迅速に応答する。 数秒以内に2xxステータスコードを返してください。DocMapはレスポンスを最大10秒待ちます。処理に時間がかかる場合は、Webhookをすぐに確認し、非同期で処理してください。

  • 重複を処理する。 まれに、Webhookが複数回配信される場合があります。必要に応じて抽出の id を使用して重複排除してください。

  • シークレットを安全に保管する。 署名シークレットは環境変数またはシークレットマネージャーに保管してください。ソースコードにハードコードしたり、バージョン管理にコミットしたりしないでください。

  • 失敗を監視する。 エンドポイントが継続的に失敗する場合は、サーバーログの確認を検討してください。DocMapは配信失敗をログに記録しますが、リトライは行いません。

DocMap API ドキュメント