Skip to content

Webhook

QuickTrust は検証結果をリアルタイムでWebhookとして通知します。

概要

本人確認処理は非同期で行われ、完了時に指定したURLにHTTP POSTリクエストが送信されます。

Webhookのメリット:

  • リアルタイムで結果を受け取れる
  • ポーリング不要
  • システムの効率化

使用例:

  • 本人確認完了時にユーザーステータスを自動更新
  • 不正検知時に管理者に通知
  • 検証結果をデータベースに自動保存

Webhook エンドポイントの要件

Webhook エンドポイントは以下の要件を満たす必要があります:

  • HTTPS を使用(本番環境では必須)
  • 5秒以内に 200 OK を返す
  • 冪等性を保証(同じイベントが複数回送信される可能性がある)
  • 署名を検証(セキュリティのため)

Webhook の設定

セッション作成時に callbackUrl を指定します:

bash
curl -X POST https://api.quicktrust.jp/v1/verification-sessions \
  -H "Authorization: Bearer qt_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://your-app.com/api/quicktrust/webhook"
  }'

スキーマ

Webhookペイロードの完全なJSON Schemaは以下からダウンロードできます:

このスキーマはZodスキーマ定義から自動生成されており、常に最新の状態が維持されます。

イベントタイプ

verification.approved

本人確認が承認されたときに送信されます。

トリガー:

  • すべての検証(OCR、書類スプーフチェック、顔照合、不正検知)が完了
  • 最終判定が「承認」と決定
json
{
  "event": "verification.approved",
  "verificationId": "vs_xxxxxxxxxx",
  "tenantId": "tenant_xxxxxxxxxx",
  "status": "approved",
  "decision": {
    "status": "approved",
    "reason": "all_checks_passed",
    "metadata": {
      "faceMatchSimilarity": 0.95,
      "livenessConfidence": 0.99,
      "documentSpoofRisk": "low",
      "ocrConfidence": 0.92
    }
  },
  "results": {
    "ocr": {
      "success": true,
      "confidence": 0.95,
      "payload": {
        "lastName": "山田",
        "firstName": "太郎",
        "birthDate": "1990-01-01",
        "address": "東京都渋谷区..."
      },
      "status": "success"
    },
    "documentSpoofCheck": {
      "risk": "low",
      "confidence": 0.95,
      "status": "success",
      "isDisplayAttack": false,
      "isForgedOrTampered": false
    },
    "faceMatch": {
      "similarity": 0.95,
      "isSamePerson": true,
      "status": "success"
    }
  },
  "processedAt": "2024-01-01T10:05:00Z"
}

verification.rejected

本人確認が拒否されたときに送信されます。

トリガー:

  • 顔照合で別人と判定
  • スプーフチェックで高リスクを検出
  • 複数の不正シグナルを検出
json
{
  "event": "verification.rejected",
  "verificationId": "vs_xxxxxxxxxx",
  "tenantId": "tenant_xxxxxxxxxx",
  "status": "rejected",
  "decision": {
    "status": "rejected",
    "reason": "face_mismatch",
    "metadata": {
      "faceMatchSimilarity": 0.45,
      "threshold": 0.85
    }
  },
  "results": {
    "faceMatch": {
      "similarity": 0.45,
      "isSamePerson": false,
      "status": "success"
    }
  },
  "processedAt": "2024-01-01T10:05:00Z"
}

verification.pending_review

人間による審査が必要な場合に送信されます。

トリガー:

  • OCR の信頼度が低い
  • スプーフチェックでリスクが検出された
  • 顔照合の類似度が閾値ギリギリ
  • 不正の疑いがあるが確定できない
json
{
  "event": "verification.pending_review",
  "verificationId": "vs_xxxxxxxxxx",
  "tenantId": "tenant_xxxxxxxxxx",
  "status": "pending_review",
  "decision": {
    "status": "pending_review",
    "reason": "low_ocr_confidence",
    "metadata": {
      "ocrConfidence": 0.65
    }
  },
  "results": {
    "ocr": {
      "success": true,
      "confidence": 0.65,
      "payload": {
        /* ... */
      },
      "status": "low_confidence"
    }
  },
  "processedAt": "2024-01-01T10:05:00Z"
}

手動レビュー後のイベント

人間による審査が完了した場合、verification.approved または verification.rejected イベントに review フィールドが追加されて送信されます。

json
{
  "event": "verification.approved",
  "verificationId": "vs_xxxxxxxxxx",
  "tenantId": "tenant_xxxxxxxxxx",
  "status": "approved",
  "processedAt": "2024-01-01T13:00:00Z",
  "decision": {
    "status": "approved",
    "reason": "Manually approved after review",
    "metadata": {}
  },
  "results": {},
  "review": {
    "id": "review_123",
    "decision": "approved",
    "reviewedBy": "reviewer@example.com",
    "reviewedAt": "2024-01-01T13:00:00.000Z",
    "reason": "Document is valid upon manual inspection"
  },
  "verificationResult": {
    "status": "approved",
    "livenessConfidence": 0.99,
    "faceMatchConfidence": 0.95,
    "documentAuthenticity": "verified",
    "userInputMatch": true,
    "preRegisteredInfoMatch": {
      "lastName": true,
      "firstName": true,
      "birthDate": true
    }
  }
}

手動レビューの判別

review フィールドが存在する場合、そのイベントは手動レビューによる判定結果です。

HTTPヘッダー

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

http
POST /webhooks/quick-trust HTTP/1.1
Host: your-app.com
Content-Type: application/json
X-QuickTrust-Signature: sha256=a1b2c3d4e5f6...
X-QuickTrust-Event: verification.approved

署名検証

Webhook の署名を検証して、リクエストが QuickTrust から送信されたものであることを確認します。

署名の仕組み

  1. QuickTrust が Webhook シークレットを生成(テナント設定時に提供)
  2. ペイロードを HMAC-SHA256 でハッシュ化
  3. X-QuickTrust-Signature ヘッダーに署名を含める

署名検証の実装(Node.js)

重要

署名検証には、パースされた JSON オブジェクトではなく、生のリクエストボディ(文字列)を使用する必要があります。

javascript
const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signature, secret) {
  // 生のボディから HMAC-SHA256 ハッシュを計算
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const expectedSignatureWithPrefix = `sha256=${expectedSignature}`;

  // タイミング攻撃を防ぐため、crypto.timingSafeEqual を使用
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignatureWithPrefix),
    );
  } catch {
    return false;
  }
}

// Express.js での使用例
// 重要: express.raw() を使用して生のボディを取得
app.post(
  '/webhooks/quick-trust',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-quicktrust-signature'];
    const secret = process.env.QUICK_TRUST_WEBHOOK_SECRET;
    const rawBody = req.body.toString('utf8');

    if (!verifyWebhookSignature(rawBody, signature, secret)) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // 署名が有効な場合、ペイロードをパースしてイベントを処理
    const payload = JSON.parse(rawBody);
    handleWebhookEvent(payload);

    res.status(200).json({ received: true });
  },
);

TIP

express.json() を使用すると、リクエストボディがパースされてしまい、署名検証が失敗します。必ず express.raw({ type: 'application/json' }) を使用してください。

リトライポリシー

Webhook 配信が失敗した場合、指数バックオフでリトライされます。

成功とみなされるレスポンス

  • HTTP ステータスコード 2xx

失敗とみなされるレスポンス

  • HTTP ステータスコード 4xx, 5xx
  • タイムアウト(5秒)
  • 接続エラー

ベストプラクティス

1. 冪等性の確保

同じイベントが複数回配信される可能性があります。verificationId を使用して重複を検出してください:

javascript
async function handleWebhook(event) {
  const { verificationId } = event;

  // 既に処理済みか確認
  const existing = await db.findProcessedEvent(verificationId);
  if (existing) {
    return; // 既に処理済み
  }

  // 処理を実行
  await processVerificationEvent(event);

  // 処理済みとして記録
  await db.markEventProcessed(verificationId);
}

2. 迅速なレスポンス

Webhook ハンドラーは迅速にレスポンスを返してください。重い処理はバックグラウンドジョブに委譲してください:

javascript
app.post(
  '/webhook',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    // すぐにレスポンスを返す
    res.status(200).send('OK');

    // バックグラウンドで処理
    const payload = JSON.parse(req.body.toString());
    await queue.add('process-verification', payload);
  },
);

3. エラーハンドリング

予期しないペイロードでもクラッシュしないようにしてください:

javascript
app.post(
  '/webhook',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    try {
      const event = JSON.parse(req.body.toString());

      switch (event.event) {
        case 'verification.approved':
          // event.review が存在すれば手動レビュー経由
          await handleApproved(event);
          break;
        case 'verification.rejected':
          // event.review が存在すれば手動レビュー経由
          await handleRejected(event);
          break;
        case 'verification.pending_review':
          await handlePendingReview(event);
          break;
        default:
          console.log('Unknown event type:', event.event);
      }

      res.status(200).send('OK');
    } catch (error) {
      console.error('Webhook processing error:', error);
      res.status(500).send('Internal Server Error');
    }
  },
);

トラブルシューティング

Webhook が届かない

  1. Webhook URL が正しいか確認
  2. ファイアウォールで HTTPS を許可しているか確認
  3. エンドポイントが5秒以内に 200 OK を返しているか確認

署名検証が失敗する

  1. Webhook シークレットが正しいか確認
  2. 生のリクエストボディを使用しているか確認(express.raw() など)
  3. 文字エンコーディングが UTF-8 か確認

イベントが重複する

  • verificationId でイベントを一意に識別し、処理済みを記録
  • データベースにユニーク制約を追加

QuickTrust eKYC