コンテンツにスキップ

クイックスタート

API を呼ぶと、実際のセッションが作られます。

  1. 管理画面で API キーを発行します。キーは発行時に一度だけ表示されます

  2. 管理画面で webhook の受信 URL(https)を登録し、署名用のシークレットを控えます

  3. 管理画面で戻り先の URL(この例では https://example.com/verification/return)を許可リストに登録します

  4. サーバーの環境変数に設定します

    ターミナルウィンドウ
    export SUPATRUST_API_BASE="(案内された API のベース URL)"
    export SUPATRUST_API_KEY="(発行した API キー)"
    export SUPATRUST_WEBHOOK_SECRET="(webhook のシークレット)"
    export DEMO_USER_ID="user_0001" # 例を動かすときだけ。currentUserId を自社のログインに置き換えたら消す
  5. 依存を入れます

    ターミナルウィンドウ
    npm install express standardwebhooks
    npm install -D @types/express @types/node # TypeScript で書く場合
// 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);
  1. 自社の画面から POST /verification/start を呼ぶと、利用者が検証 URL へ送られます(スマートフォンのブラウザで開きます)
  2. 利用者が撮影を終えて判定が出ると、/webhooks/supatrust に通知が届きます。アプリは署名を検証し、GET /v1/verification-sessions/{id} で最新の状態を取得して保存します
  3. 利用者は /verification/return に戻ってきます。アプリは、ログイン中の利用者に保存しておいた state と照合し、保存しておいたセッション ID の結果を表示します。通知がまだ届いていなければ「確認中」と表示します
  • API キーと webhook のシークレットはサーバーの環境変数にだけ置く
  • webhook の署名は、受け取ったボディのバイト列そのもので検証する
  • 同じ通知が複数回届いても、イベント ID で 1 回だけ処理する
  • 通知の中身ではなく、API から取得した最新の状態を保存する
  • 戻り先のクエリ(sessionId / state)を結果として使わない。state とセッション ID はログイン中の利用者に紐づけてサーバー側に保存し、戻り先ではその保存値で結果を引く
  • 知らないイベント種別は無視して 2xx を返す

次は セッションwebhook で、状態の意味と受信の詳細を確認してください。