스테이블코인 결제.
자금 통제권은 그대로.
MainPay는 정확한 금액의 청구서를 생성하고 판매자가 관리하는 지갑을 모니터링하며 온체인 이체를 대조한 뒤 서명된 웹훅을 보냅니다. 실제 자금을 이동하지 않고 전체 흐름을 테스트하세요.
간편 결제 연동
서버에서 생성하고, 고객의 지갑에서 결제합니다.
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 웹훅을 설정하세요. 예제에는 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확정 상태 조회
웹훅은 알림입니다. 연동 기능에 최신 상태가 필요할 때는 청구서를 직접 조회하세요.
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);
}인증
키 하나, 워크스페이스 하나, 모드 하나
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, 결제 내역이 포함되지 않습니다.
웹훅
먼저 검증하고, 한 번만 처리하세요.
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
}
}제한된 파일럿
연동을 테스트할 준비가 되셨나요?
지갑을 연결하고 웹훅을 설정한 뒤 MainPay에 워크스페이스의 테스트 인증 정보 활성화를 요청하세요. 테스트 결과가 검증되면 운영 접근 권한이 제공됩니다.