开发者 受控试点
OpenAPI支持工作台
API v1 以非托管为设计原则

接受稳定币付款。
资金始终由你掌控。

MainPay 创建精确金额的账单,监测商家控制的钱包,匹配链上转账并发送签名 Webhook。无需转移真实资金即可测试完整流程。

MainPay 从不托管资金。不执行扣款、出款、分账、托管或网关退款。付款直接结算至企业控制的钱包。

快速接入收银台

在服务端创建订单,在客户自己的钱包中付款。

原生 REST API 可直接使用内置的 fetch,无需 SDK。前端只接收公开收银台 URL。对于额度充值,由你的应用在验证 invoice.paid 后管理并更新余额。

typescript
// 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。示例需要 curljq

01

设置环境

接入时使用测试模式。测试对象在结构上与真实结算及账目隔离。

bash
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
02

查找收款钱包

选择有效钱包的 public_id,并确认其币种和网络与计划创建的账单一致。

bash
curl --fail-with-body -s "$MAINPAY_API_BASE/wallets" \
  -H "X-API-Key: $MAINPAY_API_KEY" | jq
03

创建账单

每个独立的创建请求都应使用唯一的幂等键。因传输问题重试时,复用相同的键和请求体。

bash
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
201{ "public_id": "…", "livemode": false, "status": "pending", "crypto_amount": "500.00", "hosted_url": "https://mainpay.com/pay/…" }
04

模拟检测与结算

模拟器使用与真实付款相同的状态转换和事件服务,但不会写入真实链上凭证。

bash
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
05

获取权威状态

Webhook 仅用于通知。集成需要最新状态时,请主动获取账单。

bash
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 示例同样是完整支持的接入方式。

typescript
// 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_…

正式密钥

监测资金向你自己钱包的真实结算。通过测试试点后才会开通正式访问权限。

http
X-API-Key: mp_test_xxxxxxxxxxxxxxxxxxxxxxxx

密钥仅展示一次,以哈希形式存储,限定权限范围,可立即撤销,并按工作空间和模式隔离。切勿在浏览器代码中暴露密钥。

接口参考

精简的 API 接口

POST/invoices创建账单201
GET/invoices通过游标分页列出账单200
GET/invoices/{public_id}获取权威账单状态200
POST/invoices/{public_id}/cancel取消待付款账单200
GET/checkout/{public_id}获取适合向付款方展示的收银台信息200
POST/checkout/{public_id}/events记录匿名收银台操作202
GET/wallets列出有效收款钱包200
POST/test/invoices/{public_id}/pay模拟测试付款200
OpenAPI 3.1 规范下载 YAML →

API 行为

适合付款运营的安全默认设置

01

十进制字符串

金额始终序列化为字符串,不使用浮点数。

02

幂等创建

相同的重试请求返回原始账单。请求体变化时返回 409

03

游标分页

列表返回 datahas_morenext_cursor

04

按密钥限流

每个滚动分钟允许 100 次请求,瞬时上限为每秒 20 次请求。

05

请求标识

每个响应都包含 X-Request-Id,用于支持与追踪。

06

保护付款方隐私的收银台

公开收银台不包含客户邮箱、内部 ID 或付款历史。

Webhook

先验证,再处理一次。

MainPay 使用 t=<unix>,v1=<hmac> 对原始请求体的精确字节签名。拒绝过期时间戳,使用恒定时间比较,并根据稳定的事件 id 去重。

javascript
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;
  }
}
1 读取原始字节2 验证签名3 保存事件 ID4 尽快返回 2xx

事件类型

invoice.created账单已准备好接收付款
invoice.payment_detected匹配的转账等待确认
invoice.paid精确金额或可接受金额的付款已确认
invoice.underpaid已确认金额低于接受标准
invoice.overpaid已确认金额超过账单金额
invoice.expired待付款账单已到期
invoice.cancelled商家已停止收款

事件至少投递一次,顺序不保证。重试采用持久化退避策略。嵌入的账单仅是快照,主动获取的状态才是权威依据。

错误

统一可预期的响应结构

json
{
  "error": {
    "type": "invalid_request_error",
    "code": "not_found",
    "message": "Invoice not found",
    "param": null
  }
}
400无效请求修正请求后再重试。
401身份验证检查是否提供了有效的 API 密钥。
403权限使用正确的模式和凭证权限范围。
404未找到对象不存在,或不属于当前工作空间或模式。
409冲突使用相同的键和请求体重试幂等请求。
422验证检查字段级错误列表。
429请求限流等待 Retry-After 指定的时间后,再添加随机延迟重试。
503暂停创建遵循 Retry-After;获取操作仍可使用。

受控试点

准备好测试接入了吗?

连接钱包,配置 Webhook,并联系 MainPay 为你的工作空间开通测试凭证。验证测试结果后再开通正式访问权限。

申请试点访问打开控制台