クイックスタート
API を呼ぶと、実際のセッションが作られます。
1. 準備する
Section titled “1. 準備する”-
管理画面で API キーを発行します。キーは発行時に一度だけ表示されます
-
管理画面で webhook の受信 URL(https)を登録し、署名用のシークレットを控えます
-
管理画面で戻り先の URL(この例では
https://example.com/verification/return)を許可リストに登録します -
サーバーの環境変数に設定します
ターミナルウィンドウ export SUPATRUST_API_BASE="(案内された API のベース URL)"export SUPATRUST_API_KEY="(発行した API キー)"export SUPATRUST_WEBHOOK_SECRET="(webhook のシークレット)"export DEMO_USER_ID="user_0001" # 例を動かすときだけ。currentUserId を自社のログインに置き換えたら消す -
依存を入れます
ターミナルウィンドウ npm install express standardwebhooksnpm install -D @types/express @types/node # TypeScript で書く場合
2. アプリを書く
Section titled “2. アプリを書く”// SupaTrust の組み込みを 1 本で通す Express アプリ(Node 20 以降)。// 1. POST /verification/start … セッションを作り、利用者を検証 URL へ送る// 2. GET /verification/return … 手続きを終えた利用者が戻ってくる(結果はここでは決めない)// 3. POST /webhooks/supatrust … 結果の通知を受け、詳細を API から取得して保存する//// API キーと webhook のシークレットはサーバーの環境変数にだけ置く(ブラウザやアプリに埋め込まない)。import { randomUUID } from "node:crypto";import { realpathSync } from "node:fs";import { fileURLToPath } from "node:url";import express from "express";import { Webhook, WebhookVerificationError } from "standardwebhooks";
const requireEnv = (name: string): string => { const value = process.env[name]; if (!value) throw new Error(`環境変数 ${name} を設定してください`); return value;};
const API_BASE = requireEnv("SUPATRUST_API_BASE");const API_KEY = requireEnv("SUPATRUST_API_KEY");const webhook = new Webhook(requireEnv("SUPATRUST_WEBHOOK_SECRET"));
// 自社のログインの仕組み(セッション cookie など)から、いまログインしている利用者の ID を取り出す部分。// **必ず置き換える**。例を動かすときだけ、環境変数 DEMO_USER_ID の 1 人としてふるまう// (置き換えずに使うと全員が同じ 1 人になるので、未設定なら止める)const currentUserId = (_req: express.Request): string => { const demoUserId = process.env.DEMO_USER_ID; if (!demoUserId) throw new Error("currentUserId を自社のログインの仕組みに置き換えてください"); return demoUserId;};
// 例のための置き場。本番では自社のログインセッションや DB に保存するconst verifications = new Map<string, { state: string; sessionId: string }>(); // 利用者 ID → 進行中の本人確認const processedEventIds = new Set<string>(); // 処理済みのイベント ID(本番は UNIQUE 制約付きの列)const results = new Map<string, { status: string; verdict: string | null }>(); // セッション ID → 結果
export const app = express();
// 1. セッションを作って利用者を送り出すapp.post("/verification/start", async (req, res) => { const userId = currentUserId(req); // state は推測できない値にし、戻ってきたときに照合する const state = randomUUID(); const response = await fetch(`${API_BASE}/v1/verification-sessions`, { method: "POST", headers: { authorization: `Bearer ${API_KEY}`, "content-type": "application/json" }, body: JSON.stringify({ // 管理画面で許可した URL だけが使える callbackUrl: "https://example.com/verification/return", state, // 自社側の相関 ID。個人情報は入れない(webhook にそのまま載って返ってくる) metadata: { userId }, }), }); if (response.status !== 201) { res.status(502).send("本人確認を開始できませんでした"); return; } const session = (await response.json()) as { id: string; verificationUrl: string }; // state とセッション ID は、この利用者のサーバー側に保存する(戻り先ではこちらを正とする) verifications.set(userId, { state, sessionId: session.id }); // verificationUrl は 1 回だけ使える秘密の URL。ログに出さず、この利用者にだけ渡す res.redirect(303, session.verificationUrl);});
// 2. 利用者が戻ってくる。クエリはブラウザを通ってくるので書き換えられる — 結果として使わない// セッション ID もクエリからは取らず、ログイン中の利用者に保存した値を使う(他人の戻り URL を// 開かされても、その他人の結果は出ない)。同じ URL に何度戻ってきても同じように動くapp.get("/verification/return", (req, res) => { const saved = verifications.get(currentUserId(req)); if (saved === undefined || req.query.state !== saved.state) { res.status(400).send("不正な戻り先です"); return; } // 結果は webhook(3.)で保存したものを表示する。まだ届いていなければ「確認中」を出す const result = results.get(saved.sessionId); res.send(result ? `本人確認の状態: ${result.status}` : "本人確認の結果を確認しています");});
// 3. webhook を受ける。署名はボディの生のバイト列で検証するので、このルートは express.raw で受ける// (express.json() で解釈した後のオブジェクトを文字列に戻すと、バイト列が変わって検証に失敗する)app.post("/webhooks/supatrust", express.raw({ type: "application/json" }), async (req, res) => { let event: { id: string; type: string; data: { session: { sessionId: string } } }; try { event = webhook.verify(req.body as Buffer, { "webhook-id": req.header("webhook-id") ?? "", "webhook-timestamp": req.header("webhook-timestamp") ?? "", "webhook-signature": req.header("webhook-signature") ?? "", }) as typeof event; } catch (error) { if (error instanceof WebhookVerificationError) { res.status(400).end(); return; } throw error; }
// 同じイベントが再送で複数回届くことがある。イベント ID で重複を除く if (processedEventIds.has(event.id)) { res.status(204).end(); return; }
switch (event.type) { case "verification.approved": case "verification.rejected": case "verification.needs_review": case "verification.expired": case "verification.failed": { // webhook には個人情報も詳細も載らない。最新の状態は API から取り直す const sessionId = event.data.session.sessionId; const response = await fetch( `${API_BASE}/v1/verification-sessions/${encodeURIComponent(sessionId)}`, { headers: { authorization: `Bearer ${API_KEY}` } }, ); // 2xx 以外を返すと再送される。取得に失敗したら例外で 500 にして、後の再送で取り直す if (!response.ok) throw new Error(`セッションの取得に失敗しました: ${response.status}`); const session = (await response.json()) as { status: string; outcome: { verdict: string } | null; }; // 応答には読み取った券面の項目(個人情報)も含まれる。ログやエラー通知に丸ごと出さない results.set(sessionId, { status: session.status, verdict: session.outcome?.verdict ?? null }); break; } default: // 知らないイベント種別は無視して 2xx を返す(種別は今後増えることがある) break; }
processedEventIds.add(event.id); res.status(204).end();});
// このファイルを直接起動したときだけ待ち受ける(import したときは app だけを渡す)。// 起動パスは実体に解決して比べる(symlink 経由で起動すると一致せず、黙って待ち受けないため)const entry = process.argv[1];if (entry !== undefined && realpathSync(entry) === fileURLToPath(import.meta.url)) app.listen(3000);3. 流れを確かめる
Section titled “3. 流れを確かめる”- 自社の画面から
POST /verification/startを呼ぶと、利用者が検証 URL へ送られます(スマートフォンのブラウザで開きます) - 利用者が撮影を終えて判定が出ると、
/webhooks/supatrustに通知が届きます。アプリは署名を検証し、GET /v1/verification-sessions/{id}で最新の状態を取得して保存します - 利用者は
/verification/returnに戻ってきます。アプリは、ログイン中の利用者に保存しておいたstateと照合し、保存しておいたセッション ID の結果を表示します。通知がまだ届いていなければ「確認中」と表示します
この例が守っていること
Section titled “この例が守っていること”- API キーと webhook のシークレットはサーバーの環境変数にだけ置く
- webhook の署名は、受け取ったボディのバイト列そのもので検証する
- 同じ通知が複数回届いても、イベント ID で 1 回だけ処理する
- 通知の中身ではなく、API から取得した最新の状態を保存する
- 戻り先のクエリ(
sessionId/state)を結果として使わない。stateとセッション ID はログイン中の利用者に紐づけてサーバー側に保存し、戻り先ではその保存値で結果を引く - 知らないイベント種別は無視して 2xx を返す