- PHP 100%
PaySync API에서 POST /v2/cash-receipts/{id}/send-sms가 없어져
라우트, 컨트롤러, 서비스, 요청 검증, 테스트, README에서 문자 전송을 뺌
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|---|---|---|
| app | ||
| bootstrap | ||
| config | ||
| public | ||
| routes | ||
| storage | ||
| tests | ||
| .editorconfig | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| artisan | ||
| composer.json | ||
| composer.lock | ||
| phpunit.xml | ||
| README.md | ||
PaySync 연동 예제 (PHP + Laravel)
PaySync REST API(v2)를 호출하고 PaySync 웹훅(v2)을 받는 가맹점 백엔드 예제입니다. 주문, 현금영수증, 입금 내역 API를 그대로 중계하고, 웹훅은 Standard Webhooks 서명을 검증한 뒤 처리합니다. API 명세는 PaySync API 문서를 참고하세요.
데이터베이스, 세션, 프런트엔드 없이 API 라우트만 쓰는 Laravel 13 앱입니다.
요구사항
- PHP 8.3 이상 (8.5 권장)
- Composer 2
- PaySync API 키와 웹훅 서명 키 (PaySync 대시보드에서 발급)
PHP를 설치하지 않았다면 Docker의 composer 이미지(PHP 포함)로 아래 명령을 실행할 수 있습니다.
docker run --rm -it -v "$PWD:/app" -w /app -p 8080:8080 composer:latest <명령>
# 예: ... composer:latest php artisan serve --host=0.0.0.0
설정
composer setup # composer install, .env.example → .env 복사, APP_KEY 생성
그다음 .env의 값을 채웁니다. .env는 Git에 올리지 않습니다.
| 환경 변수 | 필수 | 기본값 | 설명 |
|---|---|---|---|
PAYSYNC_BASE_URL |
https://api.paysync.kr |
PaySync API 주소 | |
PAYSYNC_API_KEY |
O | API 키 (sk_live_...). Authorization: Bearer <키>로 전송합니다. |
|
PAYSYNC_WEBHOOK_SIGNING_SECRET |
O | 웹훅 서명 키 (whsec_...) |
|
SERVER_PORT |
8080 (.env.example) |
php artisan serve 포트. 다른 언어 예제의 PORT에 해당합니다. |
PaySync 설정은 config/services.php의 paysync 항목에서 읽습니다. 설정을 캐시(php artisan config:cache)했다면 .env를 바꾼 뒤 다시 캐시하세요.
실행
php artisan serve # http://localhost:8080 (.env의 SERVER_PORT)
php artisan serve --port=8080 # 포트를 직접 지정
로그는 콘솔(stderr)에 찍힙니다. 파일로 남기려면 .env에서 LOG_CHANNEL=single로 바꾸면 storage/logs/laravel.log에 기록됩니다.
php artisan test # 테스트 (PaySync API는 Http::fake()로 대체)
vendor/bin/pint # 코드 스타일 정리
엔드포인트
| 메서드 | 경로 | PaySync API | 응답 |
|---|---|---|---|
| POST | /api/invoices |
POST /v2/invoices |
201 주문 |
| GET | /api/invoices |
GET /v2/invoices |
200 { items, totalItems } |
| 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 { items, totalItems } |
| 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 { items, totalItems } |
| GET | /api/transactions/{id} |
GET /v2/transactions/{id} |
200 입금 내역 |
| POST | /api/webhooks |
(PaySync가 호출) | 200 / 검증 실패 시 400 |
PaySync는 주문 생성을 포함한 모든 성공 응답을 HTTP 200과 code: "10001"로 돌려줍니다. 이 예제는 생성과 재발급에 201, 이메일 전송에 204를 돌려줍니다.
응답의 시각(createdAt, paidAt 등)은 Unix 초 정수입니다.
목록 조회 쿼리 파라미터 (offset 기본값 0, limit 기본값 20, 나머지는 선택이며 값이 없으면 PaySync에 전달하지 않습니다):
/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
dateFrom, dateTo는 YYYY-MM-DD 형식의 한국 시간 날짜이고 두 날짜 모두 포함합니다.
status, source, bankAccountId는 쉼표로 여러 값을 보낼 수 있습니다. (예: status=PENDING,EXPIRED) 값은 그대로 PaySync에 전달하고, 값 검사는 PaySync가 합니다.
요청 본문
- 주문 생성:
customer.name(필수),customer.email,customer.phoneNumber,amount(필수),bankAccountIds,expiresAt(Unix 초) 또는expireAfterDays(1~365),cashReceipt(type,identifier),metadata,sendGuideEmail - 주문 수정:
customer.name,customer.email,customer.phoneNumber,amount,expiresAt,extendDays,bankAccountIds,cashReceipt,resendGuideEmail. 보낸 필드만 바뀌고,null은 값을 지웁니다. (예:"expiresAt": null은 입금 기한을 없앱니다) 그래서 받은 JSON 객체를 그대로 전달합니다. - 주문 취소:
reason(선택). 본문 없이 보내도 됩니다. - 현금영수증 발급:
type(필수,PERSONAL/CORPORATE),identifier(selfIssued가true가 아니면 필수),selfIssued,amount(필수),customerName,invoiceId - 현금영수증 취소:
reason(필수),amount(선택, 빼면 남은 금액 전체) - 현금영수증 재발급:
identifier,type,selfIssued - 현금영수증 이메일 전송:
email(필수)
이 예제는 필수 필드와 JSON 타입만 확인합니다. 길이와 형식은 PaySync가 검사하고 오류 코드를 돌려줍니다.
오류 응답
오류 응답은 { "code": "...", "message": "...", "data": ... } 형식입니다. data는 PaySync가 오류에 data를 담았을 때만 붙습니다.
- 요청 값이 올바르지 않으면
400 BAD_REQUEST(항목별 내용은errors에 담깁니다. Laravel 기본값인 422 대신 400을 씁니다.) - PaySync가 4xx와
code를 돌려주면 같은 상태와code,data(예:404 20014,409 220010) - PaySync 5xx, JSON이 아닌 응답, 연결 실패,
10001이 아닌 성공 응답은502(code는 PaySync가 준 값 또는UPSTREAM_ERROR)
같은 입금자명과 금액의 결제 대기 주문이 이미 있으면 기존 주문 ID가 data에 담겨 옵니다.
{
"code": "220010",
"message": "PaySync API 요청이 거절되었습니다: 220010",
"data": { "invoiceId": "ivc_n0o1p2q3r4s5t6u7v8w9x0y1", "number": 1042 }
}
코드 목록은 에러 코드를 참고하세요.
요청 예시
# 주문 생성
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_xxx \
-H 'Content-Type: application/json' \
-d '{"extendDays":3,"customer":{"email":null}}'
# 주문 취소
curl -X POST http://localhost:8080/api/invoices/ivc_xxx/cancel \
-H 'Content-Type: application/json' \
-d '{"reason":"고객 요청으로 주문 취소"}'
웹훅
등록
PaySync 대시보드의 웹훅 페이지에서 웹훅 추가를 누르고, 받을 URL(예: https://example.com/api/webhooks)을 입력합니다.
API 버전은 v2를 고릅니다. 이 예제는 v2 페이로드를 처리합니다.
웹훅을 만들면 서명 키(whsec_...)가 생깁니다. 웹훅 목록의 보기에서 확인해 PAYSYNC_WEBHOOK_SIGNING_SECRET에 넣고 서버를 다시 시작합니다.
페이로드와 처리
v2 웹훅은 모두 같은 봉투에 담겨 옵니다. 페이로드의 시각은 ISO 8601 문자열입니다.
{
"id": "evt_f1g2h3i4j5k6l7m8n9o0p1q2",
"type": "invoice.paid",
"timestamp": "2026-04-28T03:20:12Z",
"data": { "id": "ivc_...", "status": "PAID", "paidTrigger": "AUTOMATIC_MATCHING", "...": "..." }
}
| 이벤트 | 전송 시점 |
|---|---|
invoice.created |
주문이 만들어졌습니다. |
invoice.updated |
결제 대기 주문의 내용이 바뀌었습니다. |
invoice.paid |
주문이 결제 완료되었습니다. 경로는 data.paidTrigger(AUTOMATIC_MATCHING, MANUAL_MATCHING, MANUAL_APPROVE, API_CALL)로 알 수 있습니다. |
invoice.expired |
입금 기한이 지나 주문이 만료되었습니다. |
invoice.canceled |
주문이 취소되었습니다. |
invoice.reopened |
매칭이 해제되어 결제 완료 주문이 다시 열렸습니다. |
transaction.created |
새 입금이 들어왔습니다. |
이벤트별 처리는 app/Services/WebhookService.php의 switch 문에 추가합니다. 모르는 type은 무시하고 200을 돌려줍니다.
- 서명은 파싱하기 전의 원본 본문으로 검증하고,
webhook-timestamp가 5분 넘게 차이 나면 거부합니다. - 재시도와 재전송 때문에 같은 이벤트가 여러 번 올 수 있습니다. 페이로드의
id(evt_...)로 중복을 걸러 주세요. - 이벤트가 오는 순서는 보장되지 않습니다.
- PaySync는 HTTP
200~299를 성공으로 기록하고, 그 밖의 응답이나 10초 안에 응답이 없으면 다시 보냅니다. 이 예제는200을 돌려줍니다.
로컬 테스트
PaySync는 localhost로 웹훅을 보낼 수 없으므로 ngrok 같은 터널로 로컬 서버를 외부에 엽니다.
php artisan serve
ngrok http 8080
출력된 주소 뒤에 경로를 붙여(예: https://abcd-1234.ngrok-free.app/api/webhooks) 위와 같이 웹훅으로 등록합니다.
웹훅 목록의 행 메뉴에서 테스트 이벤트 보내기를 누르거나 주문을 만들면 웹훅이 오고, 서버 로그에 정상 웹훅이 수신되었습니다: evt_... (invoice.created)가 찍힙니다.
터널 없이 직접 서명한 요청을 보내 볼 수도 있습니다. .env의 서명 키로 서명해서 전송합니다. (bash 기준)
BODY='{"id":"evt_local","type":"invoice.paid","timestamp":"2026-04-28T03:20:12Z","data":{"id":"ivc_test","paidTrigger":"API_CALL"}}'
TIMESTAMP=$(date +%s)
SIGNATURE=$(php -r '
require "vendor/autoload.php";
Dotenv\Dotenv::createImmutable(".")->load();
echo (new StandardWebhooks\Webhook($_ENV["PAYSYNC_WEBHOOK_SIGNING_SECRET"]))->sign("whm_local", $argv[1], $argv[2]);
' "$TIMESTAMP" "$BODY")
curl -i -X POST http://localhost:8080/api/webhooks \
-H "webhook-id: whm_local" \
-H "webhook-timestamp: $TIMESTAMP" \
-H "webhook-signature: $SIGNATURE" \
--data-raw "$BODY"
HTTP/1.1 200 OK가 오고 로그에 주문이 결제 완료되었습니다: ivc_test (API_CALL)가 찍히면 성공입니다.
서명이 틀리면 400을 돌려주고 웹훅 검증에 실패했습니다 경고를 남깁니다. 검증되지 않은 요청의 본문은 로그에 남기지 않습니다.
구조
bootstrap/app.php 라우트 등록, 오류 → HTTP 응답 변환 (PaySyncApiException, 검증 오류)
config/services.php PaySync 설정 (환경 변수)
routes/api.php 엔드포인트 (/api 접두사)
app/
├── PaySync/ PaySync API 클라이언트 (기본 URL, 인증, 응답 해석, PaySyncApiException)
├── Providers/ 클라이언트와 웹훅 검증기를 컨테이너에 등록
├── Services/ 도메인별 API 호출 (주문, 현금영수증, 입금 내역) + 웹훅 처리
├── Http/Controllers/ 도메인별 컨트롤러
├── Http/Requests/ 요청 검증 (FormRequest)
├── Data/ 응답 모델 (Invoice, CashReceipt, Transaction, BankAccount, Actor, Page 등)
└── Enums/ CashReceiptType (요청 검증용)
tests/Feature/ PHPUnit 기능 테스트