시작하기
test 키 → 주문 등록 → 가짜 입금 → order.paid 웹훅 → 되돌리기, 30분
실제 돈 없이 끝까지 가 봅니다. 테스트 키로 만든 주문 · 입금은 실제 데이터와 섞이지 않고, 라이브로 바꿀 때 코드는 그대로입니다.
아래 명령은 셸 변수 둘을 씁니다 — $API 는 API 서버 주소(아래 값 그대로), $KEY 는 1단계에서 발급받은 키입니다. 명령을 치기 싫으면 레퍼런스에서 키를 넣고 화면에서 보내 봐도 됩니다.
1. 테스트 키 발급
- 페이마크에 로그인한 뒤 설정 > API(
/settings/developers)로 갑니다 - 테스트 탭을 고릅니다. 처음 test 키를 발급하면 조직에 테스트 계좌 하나가 같이 만들어집니다 — 은행 통지는 오지 않고
POST /v1/test/deposits로만 입금이 들어옵니다 - 이름을 적고 권한은 읽기 · 쓰기로 발급합니다. 키 원문(
pm_test_…)은 이때 한 번만 보입니다 — 복사해 두세요
export API=http://localhost:8787
export KEY=pm_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 1단계에서 발급한 키
키는 서버에서만 씁니다. 브라우저 · 앱 코드에 넣지 마세요 — /v1 은 CORS 를 열지 않습니다.
2. 주문 등록
curl -s -X POST "$API/v1/orders" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: demo-order-1" \
-d '{ "amount": 39000, "payer_name": "홍길동", "external_id": "A-20261006-001", "item_name": "봄 시즌 원피스" }'
201 과 함께 주문 객체가 옵니다. id(ord_…)와 payment_url 을 봐 두세요. payment_url 은 구매자에게 보내는 결제 안내 화면입니다 — 테스트 주문은 화면 위에 "테스트 주문입니다" 띠가 붙습니다.
bank_account_id를 생략했습니다 — test 키는 계좌가 테스트 계좌 하나라 됩니다. 라이브에서 계좌가 둘 이상이면 넣어야 합니다Idempotency-Key는 재시도 때 주문이 두 개 생기지 않게 합니다. 같은 키 · 같은 본문이면 처음 응답이 그대로 옵니다- 같은
external_id로 다른 내용을 보내면 409external_id_conflict입니다 — 조용히 덮어쓰지 않습니다
3. 웹훅 주소 등록 (받는 서버가 있으면)
설정 > API > 테스트 탭 > 웹훅에서 주소를 추가합니다. 조건은 https 하나입니다 — 로컬에서 개발 중이면 ngrok · cloudflared 같은 터널의 https 주소를 씁니다. 리다이렉트는 따라가지 않으니 최종 주소를 등록합니다.
받는 쪽은 서명을 검증하고 2xx 로 바로 답하면 됩니다. 아래는 Node 예시이고, 다른 언어와 자세한 규칙은 웹훅 받기에 있습니다.
import http from "node:http"
import { Webhook } from "standardwebhooks" // npm i standardwebhooks
const wh = new Webhook(process.env.WEBHOOK_SECRET) // 설정 > API > 웹훅에서 보는 whsec_…
http.createServer((req, res) => {
let body = ""
req.on("data", (c) => { body += c })
req.on("end", () => {
let event
try {
event = wh.verify(body, req.headers) // 서명 · 시각이 틀리면 throw
} catch {
res.writeHead(400).end()
return
}
res.writeHead(200).end() // 먼저 답하고
if (event.type === "order.paid") { // 그다음 처리
console.log("결제됨", event.data.object.id, event.data.object.payment)
}
})
}).listen(3000)
받는 서버가 아직 없어도 됩니다. 4단계 뒤에 GET /v1/events 로 같은 이벤트를 끌어올 수 있습니다.
4. 가짜 입금
curl -s -X POST "$API/v1/test/deposits" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "amount": 39000, "counterparty": "홍길동" }'
이 호출은 은행 통지가 온 것과 같은 길을 탑니다 — 입금 저장 → 매칭 → 이벤트 → 웹훅. 201 로 돌아오는 거래 객체의 status 가 매칭 결과입니다. 금액과 입금자명이 2단계 주문과 같으니 confirmed 이고 order_id 가 그 주문입니다.
같은 금액의 열린 주문을 둘 만들어 두고 입금을 넣으면 held(보류)가 되고 transaction.held 이벤트가 납니다 — 이때는 POST /v1/transactions/{id}/match 로 어느 주문인지 알려 줍니다.
5. order.paid 확인
웹훅을 등록했다면 받는 서버에 이런 본문이 왔습니다(값은 예시).
{
"id": "evt_8f3k2m9d1a2b3c4d5e6f7a8b9c0d1e2f",
"type": "order.paid",
"livemode": false,
"created_at": "2026-10-06T14:11:52+09:00",
"data": {
"object": {
"id": "ord_3f9a0c1e2b4d4f6a8c0e1a2b3c4d5e6f",
"object": "order",
"status": "paid",
"amount": 39000,
"payer_name": "홍길동",
"external_id": "A-20261006-001",
"payment": {
"transaction_id": "txn_7c2e1b9a0d4f4e3a8b1c2d3e4f5a6b7c",
"amount": 39000,
"counterparty": "홍길동",
"occurred_at": "2026-10-06T14:11:00+09:00",
"matched_at": "2026-10-06T14:11:52+09:00",
"decided_by": "auto",
"trigger": "auto",
"reason": "exact"
}
}
}
}
data.object.payment 가 어떤 입금으로 결제됐는지입니다. 주문 처리는 external_id 또는 id 로 내 주문을 찾아 payment.transaction_id 를 적어 두면 됩니다.
받는 서버가 없으면 이벤트를 직접 읽습니다.
curl -s "$API/v1/events?type=order.paid" -H "Authorization: Bearer $KEY"
curl -s "$API/v1/orders/ord_…" -H "Authorization: Bearer $KEY" # status 가 paid, payment 가 채워져 있다
6. 되돌리기
잘못 맞춘 입금은 확정 뒤 24시간 안에 되돌릴 수 있습니다.
curl -s -X POST "$API/v1/orders/ord_…/reverse" -H "Authorization: Bearer $KEY"
주문은 다시 open, 입금은 unmatched 가 되고 order.payment_reversed 이벤트가 납니다 — data.previous_payment 에 되돌린 매칭이 있습니다. 이미 출고했다면 여기서 멈춰 세우는 것이 이 이벤트의 쓰임새입니다.
여기까지가 연동의 전부입니다.
라이브로 바꾸기
- 설정 > 계좌 관리에서 실제 계좌를 등록하고 은행에 입출금 SMS 통지를 신청합니다(계좌마다 안내가 뜹니다)
- 설정 > API > 라이브 탭에서
pm_live_키를 발급합니다. 계좌가 여럿이면 키의 계좌 범위를 고를 수 있습니다 - 라이브 탭에서 웹훅 주소를 다시 등록합니다 — 엔드포인트와 시크릿은 모드마다 따로입니다
- 코드에서 키만 바꿉니다.
livemode가true로 올 뿐 객체 · 이벤트 모양은 같습니다
라이브 키는 테스트 객체를 보지 못하고(404), 반대도 같습니다. POST /v1/test/deposits 는 live 키로 부르면 404 입니다.
막혔을 때
- 401 — 헤더가
Authorization: Bearer pm_…인지, 키를 폐기하지 않았는지 - 403
insufficient_scope— 읽기 전용 키로 POST 를 불렀습니다 - 400
param: bank_account_id— 범위 안 계좌가 둘 이상이거나 없습니다.GET /v1/bank-accounts로 id 를 봅니다 - 웹훅이 안 옵니다 — 설정 > API > 웹훅 > 그 주소의 전송 로그에 시도마다 상태 코드가 남습니다. "테스트 이벤트 보내기"로 연결만 따로 확인할 수 있습니다
- 그 밖은 레퍼런스의 에러 코드 표