/checkout は、URL のパラメーターだけで JPYC / USDC の支払いページを開けるリンクです。
受取ウォレットのアドレスがあれば使えます。API キー・登録・サインインは要りません
(/checkout に限ります。@handle のモバイル注文や Store は別です → 次の段階)。
OpenPay は代金を預かりません。お客様のウォレットから受取アドレスへ、ブロックチェーン上で直接届きます。
開発をしない方は、受け取りの画面の「注文リンク」タブで、商品・送料・注文番号からこのリンクを作れます。
https://open-pay.jp/ja/checkout?to=0xあなたの受取アドレス&token=jpyc&items=Coffee:1:500&order_id=A-001&success_url=https://example.com/paid
お客様はこのページでウォレットをつなぎ、表示された金額(例: 500 JPYC)を支払います。
ここに挙げたものは、互換を保って使い続けられます。これ以外のパラメーター(webhook・store_handle・gas・
利用料・税・レシート・店舗端末用のもの)は OpenPay の内部用で、予告なく変わります。
| 名前 | 必須 | 内容 |
|---|---|---|
to |
必須 | 受取アドレス(0x…) |
token |
必須 | jpyc または usdc |
chain |
任意 | 省略時は JPYC = polygon・USDC = base。JPYC は polygon / kaia / avalanche / ethereum、USDC は base / arbitrum / optimism / polygon / ethereum / avalanche / arc |
items |
必須 | 名前:数量:単価 を , でつなぐ(1〜10 件・数量 1〜999・単価はトークンの小数桁以内)。名前は encodeURIComponent してから並べる |
order_id |
任意 | あなたの注文 ID(64 文字まで・超えた分は切り捨て)。success_url にそのまま返す |
description |
任意 | 説明文(200 文字まで) |
store |
任意 | 店名(48 文字まで)。お客様の画面の頭と控えに出る表示だけの値で、受取先・金額には関わらない |
ship |
任意 | ask のとき、支払いの後にお客様が完了画面の「注文を送る」で、お名前とお届け先を自分の LINE やメールでお店へ送る(注文リンクの「何度でも使える」リンク)。お客様の情報は OpenPay に届かない。ask 以外の値は無視 |
contact |
任意 | line:@ID(LINE 公式アカウント)か mail:アドレス。完了画面が、注文の文を入れた状態でこの送り先を開く。形が合わない値は無視(共有とコピーだけになる) |
chains |
任意 | 受け付けるチェーンを , でつなぐ(例: polygon,kaia)。token=jpyc のときだけ効く(token=usdc では chains を丸ごと無視し、chain(省略時は base)の 1 つのチェーンで払う)。2 つ以上で chain が無ければ、お客様が払う前にチェーンを選ぶ。そのトークン・支払い方法で使えないチェーンは無視。受取アドレス to は全チェーンで同じものを使うので、チェーンごとに持ち主が違うコントラクトのウォレット(Safe など)には使わない。モバイル注文・お店の端末の会計では無視 |
disc |
任意 | 値引きの額(items の合計から引く・JPYC は 1 単位・USDC は 0.01 単位・合計より小さい)。お客様の画面に小計・値引き・お支払い額が出て、払う額は値引きの後の額になる。形が合わない・合計以上の値はエラー画面になる(値引きなしで多く払わせないように) |
mode |
任意 | gasless(省略時)または standard。それ以外の値はエラー画面になる(打ち間違いで利用料が発生しないように) |
success_url |
任意 | 支払い完了後に移動する先(http / https) |
cancel_url |
任意 | 「中止して戻る」の移動先(http / https) |
items は、名前だけを encodeURIComponent してから組み立てると、名前に : や , があっても壊れません。
const sp = new URLSearchParams({ to, token: 'jpyc', order_id: 'A-001', success_url: 'https://example.com/paid' });
sp.set('items', [{ name: 'コーヒー', qty: 1, price: '500' }].map((i) => `${encodeURIComponent(i.name)}:${i.qty}:${i.price}`).join(','));
const link = `https://open-pay.jp/ja/checkout?${sp}`;success_url / cancel_url が OpenPay 以外のサイトのときは、支払いの前にお客様へその移動先を表示します。
- gasless(省略時): お客様はガス代用のトークン(POL など)を持っていなくても払えます。
JPYC では OpenPay 利用料がかかり、店舗の受取額から差し引かれます(お客様の支払額は表示どおり)。
チェーンやトークンの組み合わせによっては使えず、その場合は
mode=standardを求めるエラー画面になります。 JPYC の Ethereum は standard だけです。 - standard: お客様が自分のウォレットでネットワーク手数料を払います。OpenPay 利用料はかかりません。
USDC は OpenPay 利用料がかかりません。料率と条件は 料金ページ が正です (このドキュメントには数字を書いていません)。
success_url に次のクエリを付けて移動します(完了から約 3 秒後)。
| 名前 | 内容 |
|---|---|
tx_hash |
取引のハッシュ。照合の鍵はこれです(order_id はブロックチェーンに載りません) |
order_id |
渡した値(指定したときだけ) |
chain / token / mode |
支払いに使ったチェーン・トークン・方式 |
block |
ブロック番号(分かるときだけ) |
fee_tx_hash / user_op_hash |
経路によって付く補助の値 |
この値は信用しないでください。 URL はお客様の側で書き換えられます。必ず tx_hash を使って、
次の手順でブロックチェーン上の入金を確かめてください。
また、サーバー間の通知(webhook)はありません。お客様が移動の前に画面を閉じると、success_url には
何も届きません(その場合の探し方は 取りこぼしたとき)。
OpenPay の署名レシートや Payment Attestation API も使えますが、それらは OpenPay を信じる前提です。 次の手順なら、自分で用意した RPC だけで確かめられます。
- 取引が成功しているか:
eth_getTransactionReceipt(tx_hash)のstatusが成功であること。 - 自分にいくら届いたか: その取引のログのうち、トークン本体のアドレスが出した
Transferで、 宛先(to)が自分の受取アドレスのもののvalueを合計する。 同じ形のイベントは誰のコントラクトでも出せるので、ログのaddressがトークンのアドレスと一致するものだけを数えます。- JPYC v3:
0xE7C3D8C9a439feDe00D2600032D5dB0Be71C3c29(全チェーン共通・小数 18 桁) - USDC のアドレスと、ガスレス用の forwarder の一覧は 運用の透明性 にあります
- JPYC v3:
- 請求額と合っているか:
- standard / USDC: 届いた額 = 請求額。
- JPYC の gasless: 届く額は請求額から利用料を引いた額です。同じ取引にある 公式の forwarder が出した
Settledイベント (Settled(from, nonce, merchant, merchantValue, feeReceiver, feeValue))で、merchantが自分・merchantValueが届いた額・merchantValue + feeValueが請求額であることを確かめます。
- 確定しているか: 「
success_urlに戻ってきた」は確定の証拠ではありません(JPYC のガスレスは 1 ブロックの確認で完了画面になります)。- Polygon / Ethereum: 取引のブロックが
eth_getBlockByNumber("finalized")以下になり、そのブロック番号のブロック hash が receipt のblockHashと同じであることを確かめる。 - Kaia / Avalanche: すぐに確定する仕組みのチェーンです。RPC が
finalizedを受け付けない場合は、数ブロック待ってから同じように ブロック hash を確かめる。
- Polygon / Ethereum: 取引のブロックが
- 二重に数えない:
chainId + tx_hashを注文と一緒に保存し、同じ取引を 2 つの注文に使わない。 - RPC を 1 社に頼らない: 大事な金額なら、2 社以上の RPC で同じ結果(同じブロック hash)になることを確かめる。
// verify-payment.mjs — tx_hash から JPYC の入金を確かめる (OpenPay のサーバーを使わない・viem 2.x)
// 使い方: node verify-payment.mjs <RPC の URL> <tx_hash> <受取アドレス> <請求額 (JPYC)>
import { createPublicClient, http, erc20Abi, parseAbi, parseEventLogs, parseUnits, isAddressEqual } from 'viem';
import { polygon } from 'viem/chains';
const JPYC = '0xE7C3D8C9a439feDe00D2600032D5dB0Be71C3c29'; // JPYC v3 (全チェーン共通)
const FORWARDER = '0x0F4560a777415580F0680F8B56a79B0022C6B848'; // Polygon のガスレス用 (最新は /transparency で確認)
const SETTLED = parseAbi([
'event Settled(address indexed from, bytes32 indexed nonce, address indexed merchant, uint256 merchantValue, address feeReceiver, uint256 feeValue)',
]);
export async function verifyJpycPayment({ rpcUrl, txHash, merchant, amountJpyc }) {
const client = createPublicClient({ chain: polygon, transport: http(rpcUrl) });
const receipt = await client.getTransactionReceipt({ hash: txHash });
if (receipt.status !== 'success') return { ok: false, reason: 'reverted' };
// 1. JPYC 本体が出した、自分宛ての Transfer だけを数える (同じ形のイベントは誰のコントラクトでも出せる)
const received = parseEventLogs({ abi: erc20Abi, eventName: 'Transfer', logs: receipt.logs })
.filter((log) => isAddressEqual(log.address, JPYC) && isAddressEqual(log.args.to, merchant))
.reduce((sum, log) => sum + log.args.value, 0n);
if (received === 0n) return { ok: false, reason: 'no_transfer' };
// 2. ガスレスなら、公式の forwarder の Settled で「請求額 = 受取額 + 利用料」を確かめる
const settled = parseEventLogs({ abi: SETTLED, logs: receipt.logs })
.filter((log) => isAddressEqual(log.address, FORWARDER) && isAddressEqual(log.args.merchant, merchant));
if (settled.length > 1) return { ok: false, reason: 'ambiguous' };
if (settled.length === 1 && settled[0].args.merchantValue !== received) return { ok: false, reason: 'mismatch' };
const billed = settled.length === 1 ? settled[0].args.merchantValue + settled[0].args.feeValue : received;
if (billed !== parseUnits(amountJpyc, 18)) return { ok: false, reason: 'amount' };
// 3. 確定を待つ: finalized のブロック以下で、そのブロックが今も正規のチェーンにあること
const finalized = await client.getBlock({ blockTag: 'finalized' });
if (receipt.blockNumber > finalized.number) return { ok: false, reason: 'not_finalized' };
const block = await client.getBlock({ blockNumber: receipt.blockNumber });
if (block.hash !== receipt.blockHash) return { ok: false, reason: 'reorged' };
// 4. 二重計上を防ぐ鍵。注文と一緒に保存し、同じ鍵の取引を 2 つの注文に使わない
return { ok: true, key: `${polygon.id}:${receipt.transactionHash}`, received, billed };
}
if (import.meta.url === `file://${process.argv[1]}`) {
const [rpcUrl, txHash, merchant, amountJpyc] = process.argv.slice(2);
console.log(await verifyJpycPayment({ rpcUrl, txHash, merchant, amountJpyc }));
}not_finalized のときは、しばらく待ってからもう一度呼んでください。無料の公開 RPC は、古い取引の receipt を返さない
(アーカイブ扱いでトークンを求める)ことがあります。
tx_hash は誰でも Explorer で見られるので、それだけでは「この文を送った人が払った」証明になりません。
完了画面の「注文を送る」「お店に知らせる」は、送る前に払ったウォレットで次の文に personal_sign で署名してもらい、
届く文の最後の行(「確認の署名: 0x…」)に入れます(お金は動きません)。
OpenPay order confirmation
chain: <chain のスラッグ(例: polygon)>
shop: <to の受取アドレス(チェックサム付き)>
tx: <tx_hash(小文字)>
改行は \n、末尾に改行はありません。署名した人(EOA は ecrecover、コントラクトのウォレットは ERC-1271)が、
その取引で払った人と同じなら、払ったウォレットの持ち主が署名した文です。払った人は、受取アドレスか forwarder へ
0 より多い JPYC を送った人がちょうど 1 人で、JPYC の AuthorizationUsed があるなら 1 つだけでその authorizer が
同じ人のときに決まります。誰でも複数の支払いを 1 つの取引にまとめられるので、払った人が 1 人に決まらない取引の
署名は確かめられないものとして扱ってください。注文リンクのタブの「入金を確かめる」はこれをブラウザの中で確かめます。
署名が示すのは「払ったウォレットの持ち主が、このお店へのこの支払いについて署名した」ことだけです。文は転送できるので、 送ってきた人やお届け先が正しいことは示しません。
order_id はブロックチェーンに載らないので、success_url を受け取れなかった注文は
「受取アドレス × 金額 × 時刻」で探すことになります。
- 受取アドレスの
Transfer(eth_getLogs・トークンのアドレス・topics = [Transfer, null, 受取アドレス])を、注文の時刻の前後で探す。 公開 RPC は一度に検索できるブロックの範囲が狭いことがあります。 - 組み込み専用の受取アドレスを使ってください。 レジや決済 QR など他の受け取りと同じアドレスを使うと、どの支払いか区別できません。
- 同じ金額の注文を同時に受け付けていると、どの注文の支払いか決められません。その場合は人が確かめてください。
- お客様の完了画面と、お客様のブラウザに残る電子レシートには取引のハッシュが表示されます。問い合わせのときに教えてもらうこともできます。
- ガスレス決済の forwarder コントラクトは試験運用(alpha)の段階で、第三者の監査会社による監査はまだ受けていません。
- アドレスは、必ず 運用の透明性 か OpenPay の公式の発表と照合してください。
- ブロックチェーンの送金は取り消せません。本番の前に少額で試してください。
@handle(サインインして作る OpenPay のページ)を持つと、次のものも使えます。登録が要るので、上の「登録不要」には含みません。
- モバイル注文(メニュー・注文の受け付け・AI エージェントからの注文。MCP の
createOrderLinkは/@handle?cart=のリンクを作ります) - OpenPay Store(デジタル商品の販売。掲載一覧は
GET /api/store/listingsで取得できます)