2026年秋頃、Google ウォレットにマイナンバーカードが入る予定です。口座開設や携帯電話の契約、年齢確認などで、スマホの身分証を提示してもらえるようになります。
デジタル庁の発表はこちら
https://services.digital.go.jp/mynumbercard-android/news/0cfe138d7fb5927e4dc6d/
このとき事業者側に必要になるのが、提示された身分証を受け取って検証する仕組みです。提示について必要になる実装知識は、以下に記載されています。
- Digital Credentials API と OpenID4VP の使い方、検証者としての登録フロー
- Credential Manager の Verifier API
- Google Pay and Wallet APIs の利用規約
今回の記事ではGoogle Walletへパスを発行する仕組みをハンズオンで解説します。
なお、マイナンバーカードのようなデジタルIDを自分で発行することはできません。そのためのAPIは公開されておらず、汎用パスで代用することも上の利用規約で禁じられています。一方で、イベントチケットや会員証のような自前のパスなら、審査も待ちもなく今すぐ発行できます。
マイナンバーカードの様なデジタルIDがGoogle Walletに実装されることで、基本4情報以外のデータがウォレットに格納され、本人確認と同時に属性証明が行われるというユースケースを想定し、Google Walletへのパスの発行を実際にやってみました。
Webからプログラムでパスを発行し、Androidの実機で受け取って、カードのQRコードがカメラで読み取れるところまでを確かめます。
全体の流れ
- ウォレットサービスの登録(Google Pay & Wallet Console)
- GCPの準備(Google Cloud Console)
- ウォレットサービスの設定(Google Pay & Wallet Console)
- プログラム作成
- 発行の確認(Androidの実機)
1. ウォレットサービスの登録
Google Pay & Wallet Console を開いて、Googleアカウントでログインします。最初にビジネス情報の入力を求められます。

公開ビジネス名、ビジネスの種類、拠点の3つを入れて、利用規約に同意します。拠点は後から変更できないのでご注意を。
登録が終わったら、左メニューの「Google Wallet API」を開きます。ここで「Create a pass」→「Build your first pass」と進み、Wallet API の利用規約に同意すると、発行者アカウントができます。

タイトルの右に「発行者 ID」として19桁の数字が出ています(上のキャプチャでは伏せています)。これがこのあとプログラムで使う番号です。
注意が必要なのは、コンソールにはもう一つ別のIDがあることです。ダッシュボードに大きく出ている Merchant ID(BCR2 で始まる文字列)は別物で、Wallet API では使いません。
なお、作りたてのアカウントは「デモモード」です。パスを保存できるのが自分と、あとから登録するテストアカウントだけ、という状態です。今回はこのままで進めます。
2. GCPの準備
ここからは Google Cloud Console 側の作業です。プログラムから Wallet API を叩くための鍵を作ります。
まずプロジェクトを作ります。

次に、そのプロジェクトで Google Wallet API を有効にします。「APIとサービス」→「ライブラリ」で検索すると2件出てきますが、サービス名が walletobjects.googleapis.com のほうです。

続いてサービスアカウントを作ります。「IAMと管理」→「サービス アカウント」→「サービス アカウントを作成」です。

名前を入れると、wallet-issuer@プロジェクトID.iam.gserviceaccount.com のようなメールアドレスが自動で決まります。このアドレスは次のステップで使うので控えておきます。ロールを選ぶ画面が続きますが、何も選ばずに進みます。
最後に鍵を作ります。作ったサービスアカウントの「鍵」タブから「キーを追加」→「新しい鍵を作成」で、JSONを選びます。

「作成」を押すとJSONファイルがダウンロードされます。このファイルは再取得できないので、なくさないように保管します。
中身はこうなっています。
{
"type": "service_account",
"project_id": "...",
"private_key": "-----BEGIN PRIVATE KEY-----\n...",
"client_email": "wallet-issuer@....iam.gserviceaccount.com",
"token_uri": "https://oauth2.googleapis.com/token"
}プログラムから使うのは client_email、private_key、token_uri の3つです。この3つでアクセストークンを作ります。やり方は4章で書きます。
パスワードと同じ扱いのファイルなので、リポジトリには入れないようにしてください。
3. ウォレットサービスの設定
Google Pay & Wallet Console に戻ります。さっき作ったサービスアカウントに、API を呼ぶ権限を渡します。
左メニューの「ユーザー」から「Invite a user」を開きます。

メールアドレス欄に、JSON鍵の client_email をそのまま入れます。Access level は「開発元」を選びます。

「招待」を押すと、一覧に追加されます。人を招待するときと違って、承諾の操作はいりません。

これで準備は終わりです。手元にJSON鍵があり、そのサービスアカウントが発行者に登録された状態になりました。
4. プログラム作成
ここからはウォレットに対してパスを発行するコードについて解説します。今回はNode で書きました。作成したファイルの全量は以下です。それぞれのファイルについて解説します。
auth.mjs アクセストークンを取る
01_class.mjs Class を作る
02_object.mjs Object を作る
03_savelink.mjs 保存リンクを作るアクセストークンを取る
3章で発行者に登録したサービスアカウントの鍵をここで使います。
Wallet API は Authorization: Bearer ... を要求します。この値は固定の文字列ではなく、鍵から毎回作るものです。JSON鍵の private_key でJWTに署名し、token_uri に投げてアクセストークンと交換します。有効期限は1時間なので、切れたら取り直します。
import { createSign } from "node:crypto";
import { readFile } from "node:fs/promises";
const KEY_PATH = "./secrets/wallet-sa-key.json";
const key = JSON.parse(await readFile(KEY_PATH, "utf8"));
const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
const now = Math.floor(Date.now() / 1000);
const header = b64({ alg: "RS256", typ: "JWT" });
const claims = b64({
iss: key.client_email, // 誰が権限を求めているか
scope: "https://www.googleapis.com/auth/wallet_object.issuer", // Wallet発行だけに限定する
aud: key.token_uri, // このJWTの宛先
iat: now,
exp: now + 3600, // 有効期限は最長1時間
});
const signature = createSign("RSA-SHA256")
.update(`${header}.${claims}`)
.sign(key.private_key, "base64url");
const res = await fetch(key.token_uri, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: `${header}.${claims}.${signature}`,
}),
});
export const accessToken = (await res.json()).access_token;
export const serviceAccount = key;Class を作る
Class はパスのひな型です。イベント名や会員種別のような、配る相手によらず共通の情報を持ちます。100人にチケットを配るなら、Class は1つで足ります。
IDは「発行者ID.任意の名前」という形です。
import { accessToken } from "./auth.mjs";
const ISSUER_ID = "33880000000XXXXXXX"; // Wallet Console の発行者ID
const CLASS_SUFFIX = "demo_20260805";
const res = await fetch("https://walletobjects.googleapis.com/walletobjects/v1/genericClass", {
method: "POST",
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ id: `${ISSUER_ID}.${CLASS_SUFFIX}` }),
});
console.log(res.status, res.statusText);
console.log(JSON.stringify(await res.json(), null, 2));実行するとこうなります。
$ node 01_class.mjs
200 OK
{
"id": "33880000000XXXXXXX.demo_20260805"
}送ったのは id だけです。返ってきたのも id だけでした。GenericClass は必須項目がこれしかありません。カードのタイトルも色もバーコードも、表示に関わるものは全部このあとの Object 側で指定します。
同じIDでもう一度実行すると 409 Conflict が返ります。Class は作ったあと削除できません。
Object を作る
Object は実際に配る1枚です。受け取る人ごとの情報と、カードの見た目をここで指定します。
import { accessToken } from "./auth.mjs";
const ISSUER_ID = "33880000000XXXXXXX";
const CLASS_SUFFIX = "demo_20260805"; // 01_class.mjs と同じ値
const OBJECT_SUFFIX = "demo_20260805_0001"; // 配る1枚ごとに変える
const ja = (value) => ({ defaultValue: { language: "ja", value } });
const body = {
id: `${ISSUER_ID}.${OBJECT_SUFFIX}`,
classId: `${ISSUER_ID}.${CLASS_SUFFIX}`, // どのひな型の1枚か
state: "ACTIVE",
cardTitle: ja("記事デモ"),
subheader: ja("来場者"),
header: ja("山田太郎"),
barcode: { type: "QR_CODE", value: "ARTICLE-DEMO-0001" },
hexBackgroundColor: "#1a4b8c",
};
const res = await fetch("https://walletobjects.googleapis.com/walletobjects/v1/genericObject", {
method: "POST",
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
console.log(res.status, res.statusText);
console.log(JSON.stringify(await res.json(), null, 2));実行すると 200 が返り、送った内容がそのまま戻ってきます。
$ node 02_object.mjs
200 OK
{
"cardTitle": { "kind": "walletobjects#localizedString", ... },
"id": "33880000000XXXXXXX.demo_20260805_0001",
"classId": "33880000000XXXXXXX.demo_20260805",
"barcode": { "kind": "walletobjects#barcode", "type": "qrCode", ... },
"state": "active"
}Class が id だけだったのに対して、こちらは一気に増えます。この差がそのまま「どちらに何を書くか」の答えになっています。
返り値を見ると、送っていない kind が全部の要素に付いています。QR_CODE は qrCode、ACTIVE は active に変わって返ってきます。送る側と返る側で表記が違うので、レスポンスをそのまま比較すると戸惑います。
保存リンクを作る
最後に、このObjectを端末に渡すためのリンクを作ります。ここだけREST APIを呼びません。サービスアカウントの秘密鍵で署名したJWTを、そのままURLの末尾に載せます。
import { createSign } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { serviceAccount as key } from "./auth.mjs";
const ISSUER_ID = "33880000000XXXXXXX";
const OBJECT_SUFFIX = "demo_20260805_0001";
const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
const header = b64({ alg: "RS256", typ: "JWT" });
const claims = b64({
iss: key.client_email,
aud: "google",
typ: "savetowallet",
iat: Math.floor(Date.now() / 1000),
payload: { genericObjects: [{ id: `${ISSUER_ID}.${OBJECT_SUFFIX}` }] },
});
const signature = createSign("RSA-SHA256")
.update(`${header}.${claims}`)
.sign(key.private_key, "base64url");
const url = `https://pay.google.com/gp/v/save/${header}.${claims}.${signature}`;
await writeFile("secrets/save-link.txt", url + "\n");typ が savetowallet になっているのが、認証用のJWTとの違いです。payload に「どのObjectを保存させるか」を入れます。
できあがるのは680文字ほどのURLです。このURLを持っている人は誰でもそのパスを保存できます。扱いは他の秘密情報と同じにします。
5. 発行の確認
作った保存リンクをAndroidで開きます。デモモード中は、パスを保存できるのが管理者・開発元・テストアカウントに限られます。端末でログインしているGoogleアカウントが、このどれかに該当している必要があります。
リンクを開くと、追加の確認画面が出ます。

「追加」を押すと完了です。

ウォレットアプリで開くと、指定した内容が入っています。

Object を作るで解説したJSONと比較すると、スクリーンショット上部のタイトルが cardTitle、「来場者」が subheader、「山田太郎」が header、QRコードが barcode.value、背景の紺色が hexBackgroundColor です。
指定していないのに付いているものが2つあります。ひとつはタイトルの頭の「[テスト用]」。デモモードの発行者が出したパスにGoogleが自動で付けます。公開アクセスを取得すると消えます。
もうひとつは丸いアイコンです。logo は送っていませんが、header の1文字目から自動で作られています。
QRコードに入っているもの
画面に出ているQRコードを読むと、ARTICLE-DEMO-0001 という文字列が取れます。Objectを作るときに barcode.value で指定した値そのままです。
つまりQRの中身は、Googleが何かを埋め込んでいるのではなく、こちらが入れた文字列です。検証する側は、このQRを読んで得た文字列を自分のシステムで照合することになります。
ほかにどんなパスが作れるか
今回はGenericパスを使いましたが、発行できる型は7つあります。用途ごとに持てる項目が違います。
- Generic(汎用)… 会員証、駐車券、診察券など。項目を自由に決められる
- Loyalty… ポイントカード。残高や会員ランクを持てる
- Offer… クーポン。引換コードと有効期限
- Gift Card… ギフトカード。残高
- Event Ticket… イベント入場券。会場、日時、座席、ゲート
- Boarding Pass… 航空搭乗券。便名、搭乗時刻、ゲート
- Transit… 乗車券。QRコード型
型を選ぶと、その用途に必要な項目が最初から用意されていて、Wallet側もそれを理解して表示します。イベントチケットなら座席が、搭乗券ならゲートが、それらしく並びます。
Generic には決まった項目がありません。だから何にでも使えて、逆に「これは何のカードか」をWalletは理解しません。今回Genericを選んだのは、余計な項目がなく仕組みそのものが見えやすいからです。
参考リンク
この記事の手順とコードは、次のドキュメントを元に、実際に手を動かして確かめたものです。
- Google Wallet API リファレンス(Generic)https://developers.google.com/wallet/generic
- Google Pay & Wallet Console https://pay.google.com/business/console
- Save to Google Wallet のJWT仕様 https://developers.google.com/wallet/generic/web




