PAY Advent Calendar 2025
本記事は「オンライン決済サービスPAY.JPを使ってみた情報をシェアしよう! by PAY Advent Calendar 2025」アドベントカレンダーの記事です。
kintoneプラグイン内にpay.jpの決済サービスの埋め込み方の案内と、開発してみた感想です。
pay.jpで解決しようとした問題
そもそも何故pay.jpの決済サービスをkintoneプラグインに埋め込もうとしたのか?
kintoneプラグインとは、kintoneアプリをJavaScript/CSSでパワーアップするための拡張機能です。Chrome拡張機能がChromeブラウザに色々な機能を付け足してくれるように、kintoneプラグインはkintoneアプリに色々な機能を付け足します。
プラグインは外部の開発者やサイボウズの開発パートナーが開発し、ユーザに配布します。無料で使用出来るプラグインも多いですが、機能が豊富な有料なプラグインも多いです。また、戦略として、「一部の機能は無料で使えるが、支払いをすることで追加の機能が使えるようになる」というプラグインもあります。ただ、この方法でプラグインを配布するには大きな穴があるのです・・・
kintoneのプラグイン内で決済する方法が無いんです
え、じゃあ、どうやって有料機能を購入するかって・・・?
色々と方法はありますが、開発者が準備してるフォームから問い合わせをして、担当者と決済についてのやりとりをして、何かしらのライセンスキーを頂いて、それをプラグインの設定画面に入力して、それで有料機能が解放される・・・という方法を良く見かけます。
めんどうくさっ!
kintoneのプラグイン内で直接決済する方法があれば、この面倒くささと無駄な工数は回避出来るのでは?と思い、今回決済サービスが埋め込めるかどうかのPOCを作ってみました。
作ったもの
kintoneのプラグイン内にpay.jpの決済フォームを埋め込みました。初期状態だとプラグインの機能は制限されています。チェックボックスが4つあるのに、2つはグレーアウトされて操作が出来ません。決済フォームにクレジットカード情報を入力することによって機能が解放され、全てのチェックボックスが使えるようになります。
プラグイン設定画面のチェックボックスにチェックを入れると何が起こるのか。アプリの一覧画面で、表示出来るボタンが増える、というだけです。機能解放のシンプルな例です。
裏では中間サーバとしてiPaaSのMakeを使用してます。ユーザが入力したカード情報がトークン化され、Makeに送られます。Makeは受信型Webhookで情報を受取り、pay.jpに決済リクエストを送り、ChargeIDを取得します。Webhook Responseモジュールで、このChargeIDをkintoneに返しています。Makeの簡易的なデータベースにて、リクエスト元のサブドメイン名、プラグインIDやChargeIDが記録されます。
また、一度決済が済めば、決済に紐づくid(便宜上ChargeIDと呼びます)がプラグイン内に記録されます。ユーザがプラグイン設定画面を開いた際に、保存されたChargeIDをMakeの簡易的なデータベースでサーチし、一致するデータが見つかった場合に有料機能を解放してくれます。
環境の準備
pay.jp環境
- pay.jpサイトから無料のアカウントの作成をします
- APIキーを発行します。本記事では「テスト秘密鍵」と「テスト公開鍵」を使用しています。
kintone環境
- Cybozu Developer Networkの開発者ライセンスページから無料のkintone開発環境を手に入れます。
- 好きなアプリを作成します
Make環境
- Makeサイトから無料のアカウントの作成をします。
実装について
makeのシナリオ設定
シナリオは下記のような形になります。
スクリーンショットに記載されている数字の順に説明します。
- ① 受信型Webhookです。kintoneのプラグインからは、kintone.proxyを使ってこちらのエンドポイントにリクエストを投げています。
- ② HTTPリクエストのモジュールです。Webhookからトークン化されたカード情報を取得し、pay.jpへのリクエストをします。
- ③ Webhook Responseモジュールです。②で返されたChargeIDをここで設定し、kintone.proxyにレスポンスをします。
- ④ シナリオの条件分岐が設定出来るRouterです。①で受け取ったjsonにactionというキーがあり、値がcreateChargeの場合は上の処理、getStatusの場合は下の処理を進めます。
- ⑤ Makeの簡易的なデータベース、Data Storeです。リクエスト元のサブドメイン名、プラグインIDとともに、pay.jpの支払いの日時、ChargeID、そして支払いが有効かどうかのステータスが管理されています。
- ⑦ こちらもData Storeです。⑤で使用されてるData Storeと同じデータを参照しています。
- ⑧ Webhook Responseモジュールです。⑦で確認した情報をkintone.proxyにレスポンスをします。
HTTPリクエストの設定
HTTPリクエストモジュールでBasic Authを選び、Usernameには秘密鍵を設定し、Passwordには何も設定しません。
URLには決済用のエンドポイント、https://api.pay.jp/v1/charges を設定し、MethodはPostにします。Body contentに必要なパラメータを入れます。cardのkeyに、Webhookから受信したトークン化されたカード情報を設定します。
Webhook Reponseの設定
支払い処理後は、ChargeIDを返すように設定しています。
ステータス確認の際は、データベース内に記録されている支払いステータスを返すようにしています。
Data storeの設定
Makeの簡易的なデータベースには、Webhookから受信したドメインIDとプラグインID、支払い日時、pay.jpから返されたChargeIDと支払いステータスを記録しています。POCなので、支払いステータスは"Paid"に決め打ちさせてもらってます。
Makeのシナリオ内で、いつでもData Storeの中身を確認することが出来ます。必要であればこの画面からもデータは変更出来ます。
kintoneプラグイン開発
今回作ったプラグイン用のファイルの中身です。
manifestフィアル、設定用のJS/CSSファイル、アプリ用のJSファイルでまとめられています。これら全てとアイコンファイルをパッケージングすることで、プラグインの開発が出来ます。
config.html
プラグイン設定画面を構成するHTMLです。チェックボックス、pay.jp用のUIや保存・キャンセルボタンの表示などが含まれます。
<div class="container">
<h3>レコード一覧画面に表示するボタン</h3>
<label id="labelA">
<input type="checkbox" id="checkboxA">
ボタンAを表示する
</label><br>
<label id="labelB">
<input type="checkbox" id="checkboxB">
ボタンBを表示する
</label><br>
<label id="labelC" class="disabled-label">
<input type="checkbox" id="checkboxC" disabled>
ボタンCを表示する
</label><br>
<label id="labelD" class="disabled-label">
<input type="checkbox" id="checkboxD" disabled>
ボタンDを表示する
</label>
<hr>
<div id="messageArea" style="color: #fc6b03;"></div>
<div id="paymentArea">
<h3>
2500円で有料機能を有効化する
<span id="paymentStatus"></span>
</h3>
<div id="card-number"></div>
<div id="card-expiry"></div>
<div id="card-cvc"></div>
<button id="create-token">支払う</button>
</div>
<hr>
<label>
pay.jp Charge ID<br>
<input type="text" id="chargeId" disabled>
<span id="chargeStatus"></span>
</label>
<hr>
<div class="footer">
<button id="save">保存</button>
<button id="cancel">キャンセル</button>
</div>
</div>
config.js
プラグイン設定画面で走るJavaScriptです。HTML要素に設定値を入れたり、Makeへのリクエストや、レスポンスをもらった後の処理などがあります。
((PLUGIN_ID) => {
'use strict';
const PAYJP_PUBLIC_KEY = 'pk_test_xxxxx'; // テスト公開鍵
const MAKE_URL = 'https://hook.us1.make.com/xxxxx'; // Make Webhook エンドポイント
// 保存済みのプラグイン設定の取得
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
// チェックボックスと保存キャンセルのDOM取得
const checkboxA = document.getElementById('checkboxA');
const checkboxB = document.getElementById('checkboxB');
const checkboxC = document.getElementById('checkboxC');
const checkboxD = document.getElementById('checkboxD');
const saveBtn = document.getElementById('save');
const cancelBtn = document.getElementById('cancel');
// チェックボックスの初期設定(保存された設定が無ければ false)
checkboxA.checked = config.checkboxA === 'true';
checkboxB.checked = config.checkboxB === 'true';
checkboxC.checked = config.checkboxC === 'true';
checkboxD.checked = config.checkboxD === 'true';
// pay.jp関連のDOM取得
const messageArea = document.getElementById('messageArea');
const paymentArea = document.getElementById('paymentArea');
const paymentStatus = document.getElementById('paymentStatus');
const chargeIdInput = document.getElementById('chargeId');
const payBtn = document.getElementById('create-token');
//ChargeIDの初期設定
chargeIdInput.value = config.chargeId || '';
chargeIdInput.disabled = true;
// すでに ChargeID がある場合は状態確認
if (config.chargeId) {
checkChargeStatus(config.chargeId);
}
// ----------------------------
// PAY.JP 初期化
// ----------------------------
if (typeof Payjp === 'undefined') {
console.error('Payjp library is not loaded.');
return;
}
const payjp = Payjp(PAYJP_PUBLIC_KEY);
const elements = payjp.elements();
const cardNumber = elements.create('cardNumber');
cardNumber.mount('#card-number');
const cardExpiry = elements.create('cardExpiry');
cardExpiry.mount('#card-expiry');
const cardCvc = elements.create('cardCvc');
cardCvc.mount('#card-cvc');
// ----------------------------
// トークン生成 → Make経由での決済処理
// ----------------------------
payBtn.onclick = async () => {
const result = await payjp.createToken(cardNumber);
if (result.error) {
alert(result.error.message);
return;
}
const body = {
action: 'createCharge',
token: result.id,
domainId: location.hostname,
pluginId: PLUGIN_ID,
chargeId: chargeIdInput.value || ''
};
const res = await kintone.proxy(
MAKE_URL,
'POST',
{ 'Content-Type': 'application/json' },
JSON.stringify(body)
);
const data = JSON.parse(res[0]);
chargeIdInput.value = data.chargeId;
if (data.isPaid === true) {
updatePaymentStatus('Paid');
}
};
// ----------------------------
// 支払い状態確認
// ----------------------------
async function checkChargeStatus(chargeId) {
const body = {
action: 'getStatus',
chargeId: chargeId
};
const res = await kintone.proxy(
MAKE_URL,
'POST',
{ 'Content-Type': 'application/json' },
JSON.stringify(body)
);
const data = JSON.parse(res[0]);
if (data.status === 'Paid') {
updatePaymentStatus('Paid');
} else {
updatePaymentStatus(data.status);
}
}
// ----------------------------
// 支払いステータスに寄ってUIに反映
// ----------------------------
function updatePaymentStatus(status) {
paymentStatus.textContent = status ? `(${status})` : '';
paymentStatus.style.fontWeight = 'bold';
switch (status) {
case true: // createCharge 時(isPaid === true)
case 'Paid': // getStatus 時
paymentStatus.style.color = '#0000FF';
messageArea.innerHTML += "有料機能を解放中";
paymentArea.style.display = 'none';
// C・D を有効化
setCheckboxEnabled('labelC', 'checkboxC', true);
setCheckboxEnabled('labelD', 'checkboxD', true);
break;
default:
paymentStatus.style.color = '#DA70D6';
paymentArea.style.display = '';
// C・D を無効化
setCheckboxEnabled('labelC', 'checkboxC', false);
setCheckboxEnabled('labelD', 'checkboxD', false);
break;
}
}
function setCheckboxEnabled(labelId, checkboxId, enabled) {
const label = document.getElementById(labelId);
const checkbox = document.getElementById(checkboxId);
if (!label || !checkbox) return;
checkbox.disabled = !enabled;
if (enabled) {
label.classList.remove('disabled-label');
} else {
label.classList.add('disabled-label');
}
}
// ----------------------------
// 保存・キャンセルボタンの処理
// ----------------------------
saveBtn.onclick = () => {
const newConfig = {
chargeId: chargeIdInput.value || '',
checkboxA: checkboxA.checked ? 'true' : 'false',
checkboxB: checkboxB.checked ? 'true' : 'false',
checkboxC: checkboxC.checked ? 'true' : 'false',
checkboxD: checkboxD.checked ? 'true' : 'false'
};
kintone.plugin.app.setConfig(newConfig, () => {
window.location.href =
'/k/admin/app/flow?app=' + kintone.app.getId();
});
};
cancelBtn.onclick = () => {
window.location.href =
'/k/admin/app/' + kintone.app.getId() + '/plugin/';
};
})(kintone.$PLUGIN_ID);
注意
このコードをこのまま使うと、ユーザがコンソールから操作不可のフィールドを操作可能に変更し、設定値を変更することが出来ます。厳密に管理する場合、設定保存時に支払いステータスの確認した方が良いです。この記事では読みやすさを重視したため、そのチェックを端折りました。
config.css
プラグイン設定画面用のCSSです。特にpay.jpのフォーム部分を見やすい形にしています。
/*設定画面全体用*/
.container {
padding: 16px;
}
/*チェックボックスの使用不可表現用*/
.disabled-label {
color: #999;
}
/*保存/キャンセルボタン用*/
.footer {
margin-top: 20px;
}
/*pay.jpの外枠のUI用*/
#paymentArea {
border: 1px solid #ccc;
border-radius: 6px;
padding: 16px;
background-color: #fafafa;
width: 330px;
}
/*pay.jpの中身のUI用*/
#card-number,
#card-expiry,
#card-cvc {
border: 1px solid #ccc;
border-radius: 4px;
padding: 8px 10px;
margin-bottom: 12px;
background-color: #fff;
}
/*Payjp が自動で付与する class*/
.PayjpElement {
display: flex;
align-items: center;
width: 300px;
}
app.js
アプリ画面で走るJavaScriptです。プラグインの設定画面で保存した設定によって、画面上部にボタンを複数表示します。ボタンを表示させるだけです。押しても何も起こりません。
((PLUGIN_ID) => {
'use strict';
kintone.events.on('app.record.index.show', (event) => {
// プラグイン設定取得
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
const space = kintone.app.getHeaderMenuSpaceElement();
space.innerHTML = '';
if (config.checkboxA === 'true') space.appendChild(makeButton('ボタンA'));
if (config.checkboxB === 'true') space.appendChild(makeButton('ボタンB'));
if (config.checkboxC === 'true') space.appendChild(makeButton('ボタンC'));
if (config.checkboxD === 'true') space.appendChild(makeButton('ボタンD'));
return event;
});
function makeButton(label) {
const btn = document.createElement('button');
btn.textContent = label;
btn.style.marginRight = '8px';
return btn;
}
})(kintone.$PLUGIN_ID);
manifest.json
プラグインファイルを束ねるためのファイルです。pay.jpのライブラリを使用する必要があるので、https://js.pay.jp/v2/pay.js を読み込むようにしています。
{
"manifest_version": 1,
"version": "1.0.27",
"type": "APP",
"name": {
"ja": "スペシャルプラグイン",
"en": "Special Plug-in"
},
"description": {
"ja": "適当なプラグイン。pay.jpでの支払いで有料コンテンツを有効化する。",
"en": "A Special Plug-in"
},
"icon": "icon.png",
"config": {
"html": "config.html",
"js": [
"https://js.pay.jp/v2/pay.js",
"config.js"
],
"css": [
"config.css"
]
},
"desktop": {
"js": [
"app.js"
]
}
}
まとめ
pay.jp を埋め込むことによって、kintoneプラグイン内から決済を完了し、機能の解放をするというPOCが作れました。例外処理の対応や設定の厳密な管理をすれば、もう少しセキュリティ的にも良くなりそうです。また、POCとして視覚的に分かりやすく設定が出来るmakeを使用しましたが、実際には中間サーバーの実装はAWSやGoogleといったシステムで構築が現実的で管理しやすいかと思います。
pay.jpの使用についての気づき
最後に、今回の開発での気づきを述べます。
ChatGPTの知識がフロントエンド情報に弱かった
今回はChatGPTと相談しながらシステムの設計と実装をしていきました。
比較的早い段階で秘密鍵の話が出てきて、中間サーバーの必要性を理解することが出来ました。中間サーバーからpay.jpへのリクエストはスムーズに構築することが出来たのですが、今回結構手を焼いたのはフロントエンドの構築の方です。特に、Checkout機能でフロントを実装しようとした時、何度も失敗してしまい、その失敗の原因もChatGPTもあまり分からないようでした。
コンソールには下記のようなエラーが表示されており、何度カード情報を入れても処理が進みませんでした。
[Violation] Potential permissions policy violation:
payment is not allowed in this document.
こちら結果的に、プラットフォームの根本的なところのHTMLを変更しない限り直せないような問題で、kintoneでの対応は不可能だということが分かりました。ウェブサイトを自作してる場合は対応が可能なのですが、kintoneのようなプラットフォーム上でカスタマイズをしてる場合、対応が出来ないようです。
っというのを自分でググって理解して、結果をChatGPTに伝えたら、「そりゃそうだろう」みたいな返答が返ってきました。コノヤロー。
テストカードの使い方がわかると便利
システム構築中は何度も決済処理を試す場面が出てきます。pay.jpはテストカードを使うことにより、本物のクレジットカードで決済を試す必要がなくなります。
https://docs.pay.jp/v1/ で決済フォームの例があるので、こちらでテストカードの4242424242424242を入力してカード情報のトークン化というのを試すことが出来ます。ただし、ここで注意しないといけないのは、このページで生成したトークンは、自分のpay.jpのAPI処理には使えないという点です。あくまでも自分の公開鍵で生成したトークンが、自分の秘密鍵と合わせてpay.jpへのAPIリクエストが出来るようになっています。APIリクエストをテストする際には気をつけて下さい。
感想
今回機会があって、pay.jpを触らせて頂きました。
前々から解決してみたかった問題に挑戦することが出来たので、チャレンジしてみて良かったです。今回はとてもシンプルな決済システムを導入しましたが、定期課金を設定したり、Webhookを設定したり、様々な用途のためにカスタマイズの幅を広げられそうで面白そうなサービスでした。
kintoneプラグインに決済サービスを導入したい開発者がいましたら、ぜひこの記事を参考にしてください v(´∀`*v)












