ステーブルコイン決済。
資金の管理はそのまま。
MainPay は正確な金額の請求を作成し、事業者が管理するウォレットを監視して、オンチェーン送金を照合し、署名付き Webhook を送信します。実際の資金を動かさずに全フローをテストできます。
すぐに組み込める決済
サーバーで作成。支払いは顧客自身のウォレットで。
REST API は標準の fetch で利用でき、SDK は不要です。フロントエンドには公開決済 URL だけを渡します。クレジットのチャージでは、invoice.paid の検証後にアプリが残高を管理・更新します。
// Server only — MAINPAY_API_KEY never goes to the browser.
const response = await fetch("https://api.mainpay.com/v1/invoices", {
method: "POST",
headers: {
"X-API-Key": process.env.MAINPAY_API_KEY,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json"
},
body: JSON.stringify({
customer_name: "user_42",
description: "2,500 AI credits",
fiat_amount: "25.00",
asset: "USDC",
network: "base",
receiving_account_id: process.env.MAINPAY_WALLET_ID,
client_reference_id: "user_42",
metadata: { credits: 2500 },
purpose: "credit_topup"
})
});
if (!response.ok) throw new Error(`MainPay error: ${response.status}`);
const order = await response.json();
// Return only order.hosted_url to the browser.ホスト型決済画面は、正確な ERC-20 送金を支払者自身のウォレットで署名・送信するよう案内します。MainPay は鍵に触れず、決済資金やクレジット残高も保有しません。
クイックスタート
テストキーから支払い済みの請求まで
開始前に Base の USDC ウォレットを接続し、mp_test_… キーを作成して、ダッシュボードで公開 HTTPS Webhook を設定してください。サンプルには curl と jq が必要です。
環境を設定
連携中はテストモードを使用してください。テストオブジェクトは本番の決済・会計から構造的に分離されています。
export MAINPAY_API_BASE=https://api.mainpay.com/v1
export MAINPAY_API_KEY=mp_test_xxxxxxxxxxxxxxxxxxxxxxxx
export MAINPAY_WALLET_ID=1de70609-a510-4fb9-8691-61a8d2c21a34受取ウォレットを確認
請求予定の通貨・ネットワークに一致する有効なウォレットの public_id を選びます。
curl --fail-with-body -s "$MAINPAY_API_BASE/wallets" \
-H "X-API-Key: $MAINPAY_API_KEY" | jq請求を作成
個別の作成リクエストごとに一意の冪等性キーを使用します。通信エラーで再試行する場合は、同じキーと本文を再利用してください。
export IDEMPOTENCY_KEY="quickstart-$(date +%s)-$RANDOM"
ORDER_JSON=$(
curl --fail-with-body -sX POST "$MAINPAY_API_BASE/invoices" \
-H "X-API-Key: $MAINPAY_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg wallet "$MAINPAY_WALLET_ID" '{
description: "API services - quickstart",
fiat_amount: "500.00",
asset: "USDC",
network: "base",
receiving_account_id: $wallet,
customer_name: "Acme Corp",
customer_email: "ap@acme.example",
client_reference_id: "builder-user-42",
metadata: { credits: 2500 },
purpose: "credit_topup"
}')"
)
export INVOICE_ID=$(printf '%s' "$ORDER_JSON" | jq -r '.public_id')
printf '%s\n' "$ORDER_JSON" | jq{ "public_id": "…", "livemode": false, "status": "pending", "crypto_amount": "500.00", "hosted_url": "https://mainpay.com/pay/…" }検出と決済をシミュレーション
シミュレーターは本番決済と同じ状態遷移・イベントサービスを使用しますが、実際のオンチェーン証跡は記録しません。
curl --fail-with-body -sX POST \
"$MAINPAY_API_BASE/test/invoices/$INVOICE_ID/pay" \
-H "X-API-Key: $MAINPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scenario":"payment_detected"}' | jq
curl --fail-with-body -sX POST \
"$MAINPAY_API_BASE/test/invoices/$INVOICE_ID/pay" \
-H "X-API-Key: $MAINPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scenario":"exact"}' | jq正式な状態を取得
Webhook は通知です。連携先で最新の状態が必要な場合は、請求を取得してください。
curl --fail-with-body -s \
"$MAINPAY_API_BASE/invoices/$INVOICE_ID" \
-H "X-API-Key: $MAINPAY_API_KEY" | jq任意で使える便利なツール
型付き Node SDK を利用。
npm i @mainpay/node で @mainpay/node v0.1.0 をインストールします。同じ REST API をラップしており、上記の標準 fetch のサンプルも正式にサポートされる連携方法です。
// Optional typed convenience: @mainpay/node v0.1.0
// npm i @mainpay/node
// Server only — MAINPAY_API_KEY never goes to the browser.
import MainPay from "@mainpay/node";
const mainpay = new MainPay(process.env.MAINPAY_API_KEY);
const order = await mainpay.orders.create({
customer_name: "user_42",
description: "2,500 AI credits",
fiat_amount: "25.00",
asset: "USDC",
network: "base",
receiving_account_id: process.env.MAINPAY_WALLET_ID,
client_reference_id: "user_42",
metadata: { credits: 2500 },
purpose: "credit_topup"
});
// Return only order.hosted_url to the browser.
// import { openCheckout } from "@mainpay/node/checkout";
// openCheckout(order.hosted_url);
// In your webhook, verify raw bytes before granting access:
const event = mainpay.webhooks.constructEvent(
rawBody, signatureHeader, process.env.MAINPAY_WEBHOOK_SECRET
);
if (event.type === "invoice.paid") {
await grantCreditsOnce(event.id, event.data.invoice);
}認証
1 つのキー、1 つのワークスペース、1 つのモード
mp_test_…テストキー
分離されたテスト請求を作成し、すべての支払い結果をシミュレーションして、livemode:false イベントを受信します。
mp_live_…本番キー
自分のウォレットへの実際の決済を監視します。本番アクセスはテストパイロットの合格後にのみ有効になります。
X-API-Key: mp_test_xxxxxxxxxxxxxxxxxxxxxxxxキーは一度だけ表示され、ハッシュ化して保存されます。権限範囲が設定され、即時失効が可能で、ワークスペースとモードごとに分離されています。ブラウザーのコードに公開しないでください。
エンドポイントリファレンス
必要な機能に絞った API
/invoices請求を作成201/invoicesカーソルページネーションで請求一覧を取得200/invoices/{public_id}請求の正式な状態を取得200/invoices/{public_id}/cancel未決済の請求をキャンセル200/checkout/{public_id}支払者向けに情報を制限した決済画面を取得200/checkout/{public_id}/events匿名の決済ステップを記録202/wallets有効な受取ウォレット一覧を取得200/test/invoices/{public_id}/payテスト支払いをシミュレーション200API の動作
決済業務に適した安全な初期設定
小数の文字列
金額は常に文字列でシリアライズし、浮動小数点数は使いません。
冪等な作成
同一の再試行では元の請求を返します。本文を変更すると 409 を返します。
カーソルページネーション
一覧は data、has_more、next_cursor を返します。
キーごとの制限
ローリングウィンドウで毎分 100 リクエスト、バーストは毎秒 20 リクエストまでです。
リクエストの識別
各レスポンスにはサポートと追跡用の X-Request-Id が含まれます。
支払者の情報を保護する決済画面
公開決済画面には顧客のメールアドレス、内部 ID、支払い履歴を含めません。
Webhook
先に検証。処理は一度だけ。
MainPay は t=<unix>,v1=<hmac> を用いてリクエスト本文の元のバイト列に署名します。古いタイムスタンプを拒否し、定時間比較を行い、安定したイベントの id で重複を排除してください。
import crypto from "node:crypto";
export function verifyMainPaySignature(rawBody, header, secret) {
try {
const parts = Object.fromEntries(
header.split(",").map((part) => part.split("=", 2))
);
const timestamp = Number(parts.t);
if (!Number.isFinite(timestamp) ||
Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto.createHmac("sha256", secret)
.update(`${timestamp}.`).update(rawBody).digest("hex");
const supplied = parts.v1 ?? "";
const a = Buffer.from(expected);
const b = Buffer.from(supplied);
return a.length === b.length && crypto.timingSafeEqual(a, b);
} catch {
return false;
}
}イベントの種類
invoice.created請求の支払い準備が完了invoice.payment_detected一致する送金の承認待ちinvoice.paid正確な金額または受け入れ可能な支払いが確定invoice.underpaid確定額が受入基準を下回ったinvoice.overpaid確定額が請求額を超えたinvoice.expired未決済の請求が期限切れinvoice.cancelled事業者が回収を停止イベントは最低 1 回配信され、順序は保証されません。再試行には永続化されたバックオフを使用します。埋め込まれた請求はスナップショットとして扱い、直接取得した状態を正式な情報としてください。
エラー
予測可能なレスポンス構造
{
"error": {
"type": "invalid_request_error",
"code": "not_found",
"message": "Invoice not found",
"param": null
}
}限定パイロット
連携のテストを始めますか?
ウォレットを接続して Webhook を設定し、ワークスペースのテスト認証情報を有効にするよう MainPay に依頼してください。テスト結果の確認後、本番アクセスが有効になります。