Appearance
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 から送信されたものであることを確認します。
署名の仕組み
- QuickTrust が Webhook シークレットを生成(テナント設定時に提供)
- ペイロードを HMAC-SHA256 でハッシュ化
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 が届かない
- Webhook URL が正しいか確認
- ファイアウォールで HTTPS を許可しているか確認
- エンドポイントが5秒以内に
200 OKを返しているか確認
署名検証が失敗する
- Webhook シークレットが正しいか確認
- 生のリクエストボディを使用しているか確認(
express.raw()など) - 文字エンコーディングが UTF-8 か確認
イベントが重複する
verificationIdでイベントを一意に識別し、処理済みを記録- データベースにユニーク制約を追加