- Go 100%
PaySync API에서 POST /v2/cash-receipts/{id}/send-sms가 제거되어
샘플의 문자 전송 라우트, 서비스, 요청 모델, 테스트, README 설명을 지웁니다.
Popbill 에러 data 전달 테스트는 이메일 전송으로 옮깁니다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|---|---|---|
| cmd/server | ||
| internal | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
PaySync 샘플 백엔드 (Go)
PaySync REST API(v2)를 호출하고 PaySync 웹훅(v2)을 받는 가맹점 백엔드 예제입니다.
Go 표준 라이브러리(net/http, encoding/json, log/slog)만으로 작성했고, 웹훅 서명 검증에는
Standard Webhooks 공식 Go 라이브러리를 사용합니다.
PaySync API 문서: https://docs.paysync.kr/api-reference
요구사항
- Go 1.27 이상 (
go.mod의go 1.27.1)- Go 1.21 이상이 설치되어 있다면 필요한 툴체인을 자동으로 내려받습니다.
- PaySync API 키, 웹훅 서명 키(
whsec_...)
프로젝트 구조
cmd/server/ main: 설정 읽기, 라우트 등록, 서버 실행/정상 종료
internal/config/ 환경 변수 → Config
internal/paysync/ PaySync API 클라이언트 (인증 헤더, 응답 해제, APIError, 공통 Actor)
internal/httpx/ JSON 응답, 쿼리 파라미터 파싱, 에러 → HTTP 응답 매핑
internal/invoice/ 주문 (model / service / handler)
internal/cashreceipt/ 현금영수증 (model / service / handler)
internal/transaction/ 입금 내역 (model / service / handler)
internal/webhook/ 웹훅 수신, 서명 검증, 이벤트별 분기
모든 PaySync 호출은 paysync.Do / paysync.DoPage 한 곳을 거칩니다. 이곳에서 base URL과
Authorization: Bearer <API 키> 헤더를 붙이고, 응답을 풀어 성공 code("10001")인지 확인합니다.
PaySync v2는 생성을 포함한 모든 성공 응답이 HTTP 200과 "10001"입니다.
| PaySync 응답 | 형식 |
|---|---|
| 단건 | { "code", "data" } |
| 목록 | { "code", "items", "totalItems" } (items가 최상위에 있습니다) |
| 데이터 없음 | { "code" } |
| 에러 | { "code" } 또는 { "code", "data" } |
실패하면 *paysync.APIError를 반환하고, httpx.WriteError가 이를 HTTP 응답으로 바꿉니다.
API 응답의 시각은 Unix 초(정수)이고, 웹훅 페이로드의 시각만 ISO 8601 문자열입니다.
응답에 모르는 필드가 있어도 무시합니다.
설정
환경 변수로 설정합니다. 목록은 .env.example에 있습니다.
| 환경 변수 | 필수 | 기본값 | 설명 |
|---|---|---|---|
PAYSYNC_BASE_URL |
https://api.paysync.kr |
PaySync API 주소 | |
PAYSYNC_API_KEY |
✔ | PaySync API 키 | |
PAYSYNC_WEBHOOK_SIGNING_SECRET |
✔ | 웹훅 서명 키 (whsec_...) |
|
PORT |
8080 |
샘플 서버 포트 |
Go는 .env 파일을 자동으로 읽지 않습니다. 셸에서 직접 설정하세요.
# macOS / Linux
export PAYSYNC_API_KEY=발급받은_API_키
export PAYSYNC_WEBHOOK_SIGNING_SECRET=whsec_...
# 또는 .env 파일을 만들어 한 번에 불러오기
cp .env.example .env # 값 채우기
set -a; . ./.env; set +a
# Windows PowerShell
$env:PAYSYNC_API_KEY = "발급받은_API_키"
$env:PAYSYNC_WEBHOOK_SIGNING_SECRET = "whsec_..."
실행
go run ./cmd/server
Ctrl+C(또는 SIGTERM)를 받으면 처리 중인 요청을 마무리한 뒤 종료합니다.
테스트와 정적 검사는 다음과 같이 실행합니다. 테스트는 가짜 PaySync 서버(httptest)를 사용하므로
API 키가 필요 없습니다.
go test ./...
go vet ./...
Go가 설치되어 있지 않다면 Docker로 실행할 수 있습니다.
docker run --rm -v "$PWD":/src -w /src golang:1-alpine go test ./...
엔드포인트
| 샘플 서버 | PaySync API | 성공 응답 |
|---|---|---|
POST /api/invoices |
POST /v2/invoices |
201 + 주문 |
GET /api/invoices |
GET /v2/invoices |
200 + 목록 |
GET /api/invoices/{id} |
GET /v2/invoices/{id} |
200 + 주문 (처리 이력 history 포함) |
PATCH /api/invoices/{id} |
PATCH /v2/invoices/{id} |
200 + 주문 |
POST /api/invoices/{id}/mark-paid |
POST /v2/invoices/{id}/mark-paid |
200 + 주문 |
POST /api/invoices/{id}/cancel |
POST /v2/invoices/{id}/cancel |
200 + 주문 |
POST /api/cash-receipts |
POST /v2/cash-receipts |
201 + 현금영수증 |
GET /api/cash-receipts |
GET /v2/cash-receipts |
200 + 목록 |
GET /api/cash-receipts/{id} |
GET /v2/cash-receipts/{id} |
200 + 현금영수증 |
POST /api/cash-receipts/{id}/cancel |
POST /v2/cash-receipts/{id}/cancel |
200 + 취소 영수증 |
POST /api/cash-receipts/{id}/reissue |
POST /v2/cash-receipts/{id}/reissue |
201 + 새 현금영수증 |
GET /api/cash-receipts/{id}/urls |
GET /v2/cash-receipts/{id}/urls |
200 + { view, print, pdf } |
POST /api/cash-receipts/{id}/send-email |
POST /v2/cash-receipts/{id}/send-email |
204 |
GET /api/transactions |
GET /v2/transactions |
200 + 목록 |
GET /api/transactions/{id} |
GET /v2/transactions/{id} |
200 + 입금 내역 |
POST /api/webhooks |
(PaySync → 샘플 서버) | 200 |
요청 본문
샘플 서버는 필수 값이 있는지와 JSON 타입만 확인합니다. 길이와 형식은 PaySync가 검사하고 code로 알려 줍니다.
값이 없는 선택 필드는 PaySync로 보내지 않습니다.
- 주문 생성:
customer{name (필수), email, phoneNumber},amount(필수),bankAccountIds,expiresAt(Unix 초),expireAfterDays(1~365),cashReceipt{type, identifier},metadata,sendGuideEmail - 주문 수정 (
PATCH):customer.name,customer.email,customer.phoneNumber,amount,expiresAt,extendDays,bankAccountIds,cashReceipt,resendGuideEmail. 필드를 빼면 그대로 두고null을 보내면 지우므로 (customer.email,customer.phoneNumber,expiresAt,cashReceipt), 샘플 서버는 받은 JSON 객체를 그대로 전달합니다. - 주문 취소:
{ "reason": "..." }(선택, 200자까지). 본문 없이 보내도 됩니다. - 현금영수증 발급:
type(필수,PERSONAL/CORPORATE),identifier(selfIssued가true가 아니면 필수),selfIssued,amount(필수),customerName,invoiceId - 현금영수증 취소:
reason(필수, 1~100자),amount(빼면 남은 금액 전체) - 현금영수증 재발급:
identifier,type,selfIssued - 현금영수증 이메일 전송:
email(필수)
목록 조회 쿼리 파라미터
offset 기본값 0, limit 기본값 20(최대 100)이고, 나머지는 값이 있을 때만 PaySync로 전달합니다.
날짜는 YYYY-MM-DD(한국 시간, 그날 포함)입니다. status, source, bankAccountId는 여러 값을 쉼표로 이어
보낼 수 있습니다(status=PENDING,EXPIRED). 파라미터를 반복해 보내면 쉼표로 이어 전달합니다.
/api/invoices:offset,limit,search,status,due,source,createdBy,dateFrom,dateTo/api/transactions:offset,limit,search,bankAccountId,status,matchMethod,dateFrom,dateTo/api/cash-receipts:offset,limit,search,status,type,method,kind,invoiceId,dateFrom,dateTo
목록 응답은 모두 { "items": [...], "totalItems": 0 } 형식입니다.
예시
# 주문 생성
curl -X POST http://localhost:8080/api/invoices \
-H 'Content-Type: application/json' \
-d '{"customer":{"name":"홍길동"},"amount":50000,"expireAfterDays":1,"metadata":{"orderId":"ORDER-0001"}}'
# 결제 대기 주문 목록
curl 'http://localhost:8080/api/invoices?status=PENDING&limit=10'
# 입금 기한 3일 연장, 구매자 이메일 지우기
curl -X PATCH http://localhost:8080/api/invoices/ivc_... \
-H 'Content-Type: application/json' \
-d '{"extendDays":3,"customer":{"email":null}}'
# 주문 취소
curl -X POST http://localhost:8080/api/invoices/ivc_.../cancel \
-H 'Content-Type: application/json' \
-d '{"reason":"고객 요청으로 주문 취소"}'
에러 응답
에러는 { "code": "...", "message": "...", "data": ... } 형식입니다. data는 PaySync가 에러와 함께
보낸 값이 있을 때만 붙습니다. 예를 들어 같은 입금자명과 금액의 결제 대기 주문이 이미 있으면 기존 주문을 알려 줍니다.
{
"code": "220010",
"message": "주문 생성에 실패했습니다.",
"data": { "invoiceId": "ivc_n0o1p2q3r4s5t6u7v8w9x0y1", "number": 1042 }
}
| 상황 | 상태 코드 | code |
|---|---|---|
| 잘못된 요청 본문 / 쿼리 파라미터 | 400 | BAD_REQUEST |
PaySync가 4xx와 code를 응답 |
PaySync의 상태 코드 (예: 409) | PaySync의 code (예: "220010") |
그 밖의 PaySync 실패 (5xx, JSON이 아닌 응답, 네트워크 오류, "10001"이 아닌 성공 응답) |
502 | PaySync의 code 또는 UPSTREAM_ERROR |
코드 목록은 에러 코드 문서에 있습니다.
웹훅
POST /api/webhooks는 webhook-id, webhook-timestamp, webhook-signature 헤더와 파싱하기 전의
원본 본문으로 서명을 검증합니다 (internal/webhook/handler.go).
v2 웹훅은 모든 이벤트가 같은 봉투에 담겨 옵니다. data의 시각은 ISO 8601 문자열입니다.
{
"id": "evt_f1g2h3i4j5k6l7m8n9o0p1q2",
"type": "invoice.paid",
"timestamp": "2026-04-28T03:20:12Z",
"data": { "id": "ivc_...", "status": "PAID", "paidTrigger": "AUTOMATIC_MATCHING", "...": "..." }
}
- 검증 성공:
정상 웹훅이 수신되었습니다: <본문>과 이벤트 ID를 로그로 남기고,type별로 분기한 뒤 200을 응답합니다. 분기하는handle함수에 가맹점의 비즈니스 로직을 추가하세요.invoice.created,invoice.updated,invoice.paid,invoice.expired,invoice.canceled,invoice.reopened:data는 주문입니다. 결제 완료 경로는data.paidTrigger(AUTOMATIC_MATCHING,MANUAL_MATCHING,MANUAL_APPROVE,API_CALL)로 알 수 있습니다.transaction.created:data는 새 입금입니다.- 모르는
type도 무시하고 200을 응답합니다.
- 같은 이벤트가 재시도나 재전송으로 여러 번 올 수 있습니다. 페이로드의
id(evt_...)로 중복을 거르세요. 이벤트가 오는 순서는 보장되지 않습니다. - PaySync는 HTTP
200~299를 성공으로 기록합니다. 리디렉션은 따라가지 않고 실패로 기록합니다. PaySync는 응답을 10초까지 기다리므로, 오래 걸리는 작업은 200을 먼저 응답한 뒤 큐나 백그라운드 작업으로 넘기세요. - 검증 실패:
웹훅 검증에 실패했습니다경고 로그를 남기고 400을 응답합니다. 검증되지 않은 본문은 로그에 남기지 않습니다. - 전송 시각(
webhook-timestamp)이 현재와 5분 넘게 차이 나면 검증에 실패합니다.
웹훅 등록
대시보드 웹훅에서 웹훅 추가를 누르고, 받을 URL에
https://<내 서버 주소>/api/webhooks를 입력합니다. API 버전은 v2를 고르세요. 웹훅을 만들면 웹훅 목록의
보기에서 서명 키(whsec_...)를 확인할 수 있습니다. 이 값을 PAYSYNC_WEBHOOK_SIGNING_SECRET에 넣으세요.
로컬에서 테스트하기
직접 서명한 요청을 보내 확인할 수 있습니다. 서버와 같은 서명 키를 사용하세요.
SECRET="$PAYSYNC_WEBHOOK_SIGNING_SECRET"
ID=whm_local_test
TS=$(date +%s)
BODY='{"id":"evt_local","type":"invoice.paid","timestamp":"2026-04-28T03:20:12Z","data":{"id":"ivc_test","paidTrigger":"API_CALL"}}'
KEY_HEX=$(printf '%s' "${SECRET#whsec_}" | base64 -d | od -An -tx1 | tr -d ' \n')
SIG=$(printf '%s.%s.%s' "$ID" "$TS" "$BODY" \
| openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY_HEX" -binary | base64)
curl -i -X POST http://localhost:8080/api/webhooks \
-H "webhook-id: $ID" \
-H "webhook-timestamp: $TS" \
-H "webhook-signature: v1,$SIG" \
--data "$BODY"
200이 오면 성공입니다. 로그에 주문이 결제 완료되었습니다: ivc_test (API_CALL)가 남습니다.
webhook-signature 값을 바꿔 보내면 400이 옵니다.
PaySync가 보내는 실제 웹훅을 받으려면 공개 주소가 필요합니다 (localhost는 등록할 수 없습니다).
cloudflared 나
ngrok 같은 터널로 로컬 서버를 노출하고, 대시보드
웹훅에 https://<터널 주소>/api/webhooks를 등록하세요.
등록한 뒤 웹훅 목록의 행 메뉴에서 테스트 이벤트 보내기로 서명 검증까지 확인할 수 있습니다.