接受稳定币付款。
资金始终由你掌控。
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。SDK 封装相同的 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);
}身份验证
一个密钥、一个工作空间、一种模式
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商家已停止收款事件至少投递一次,顺序不保证。重试采用持久化退避策略。嵌入的账单仅是快照,主动获取的状态才是权威依据。
错误
统一可预期的响应结构
{
"error": {
"type": "invalid_request_error",
"code": "not_found",
"message": "Invoice not found",
"param": null
}
}受控试点
准备好测试接入了吗?
连接钱包,配置 Webhook,并联系 MainPay 为你的工作空间开通测试凭证。验证测试结果后再开通正式访问权限。