QR 결제 API 문서

p2p 시스템 가맹점 연동 가이드

Base URL: https://nexva.giftp2p.net
버전: v2 | 최종 수정: 2026-04-07
인증

모든 인증 API 요청에는 Bearer 토큰이 필요합니다.

Authorization: Bearer {YOUR_API_TOKEN}
Content-Type: application/json
Accept: application/json
토큰은 가맹점 관리 페이지 > QR 결제 API > API 설정에서 발급받을 수 있습니다.
공개 API (GET /api/qr-pay/{code}, GET /api/qr-pay/{code}/status, GET /api/qr-pay/{code}/qr.png)는 토큰 없이 호출 가능합니다.
POST QR 결제 생성
/api/qr-pay

QR 결제를 생성하고 결제 정보를 반환합니다. 응답의 qr_image_url로 QR 이미지를 표시하거나, payment_url을 직접 공유하세요.

요청 파라미터
파라미터필수타입설명
amount필수integer결제 금액 (최소 1,000원)
description선택string결제 설명 (최대 255자)
external_id선택string가맹점 주문번호 등 외부 식별자 (최대 100자). 콜백에 그대로 반환됩니다.
customer_id선택string가맹점 회원 식별자 (최대 100자). 콜백에 포함되어 회원 매핑에 활용합니다.
expires_minutes선택integer만료 시간(분). 기본 30분, 최소 5분, 최대 1440분(24시간)
요청 예시
curl -X POST https://nexva.giftp2p.net/api/qr-pay \
  -H "Authorization: Bearer {TOKEN}" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "amount": 50000,
    "description": "주문 #1234 결제",
    "external_id": "ORDER-1234",
    "customer_id": "CUST-5678",
    "expires_minutes": 60
  }'
응답 예시 200
{
  "success": true,
  "payment": {
    "id": 42,
    "payment_code": "A1B2C3D4E5F6",
    "amount": 50000,
    "description": "주문 #1234 결제",
    "external_id": "ORDER-1234",
    "customer_id": "CUST-5678",
    "status": "pending",
    "expires_at": "2026-04-06 16:00:00",
    "payment_url": "https://nexva.giftp2p.net/pay?code=A1B2C3D4E5F6",
    "qr_image_url": "https://nexva.giftp2p.net/api/qr-pay/A1B2C3D4E5F6/qr.png"
  }
}
payment_url — 고객에게 직접 공유하거나, QR 코드에 인코딩할 결제 URL입니다.
qr_image_url — 서버에서 생성된 QR 이미지(PNG)입니다. <img src="...">로 바로 사용 가능합니다.
GET QR 이미지 (PNG)
/api/qr-pay/{payment_code}/qr.png

인증 없이 호출 가능합니다. 결제 URL이 인코딩된 QR 코드 이미지를 PNG 형식으로 반환합니다.

사용 방법
<!-- HTML에서 바로 사용 -->
<img src="https://nexva.giftp2p.net/api/qr-pay/A1B2C3D4E5F6/qr.png" alt="QR 결제" width="220">

<!-- 또는 create API 응답의 qr_image_url 사��� -->
<img :src="payment.qr_image_url" alt="QR 결제">
QR 라이브러리 없이 서버에서 생성된 이미지를 바로 사용할 수 있습니다.
응답 헤더: Content-Type: image/png, 캐시: 1시간 (Cache-Control: public, max-age=3600)
GET 결제 정보 조회
/api/qr-pay/{payment_code}

결제 정보를 조회합니다. 인증 없이 호출 가능합니다. 인증된 유저의 경우 보유 상품권 목록도 포함됩니다.

응답 예시 200
{
  "payment_code": "A1B2C3D4E5F6",
  "amount": 50000,
  "description": "주문 #1234 결제",
  "external_id": "ORDER-1234",
  "customer_id": "CUST-5678",
  "status": "pending",
  "merchant_name": "테스트 ���맹점",
  "merchant_user_id": 10,
  "organization_name": "가��점A",
  "expires_at": "2026-04-06 16:00:00",
  "payment_url": "https://nexva.giftp2p.net/pay?code=A1B2C3D4E5F6",
  "qr_image_url": "https://nexva.giftp2p.net/api/qr-pay/A1B2C3D4E5F6/qr.png",
  "my_cards": [
    { "id": 100, "serial_no": "GC001", "brand_name": "행복", "face_value": 50000 }
  ],
  "my_total": 50000,
  "shortfall": 0
}

my_cards, my_total, shortfall는 인증된 유저 + 결제 대기 상태일 때만 포함됩니다.

POST 결제 실행 (상품권 결제)
/api/qr-pay/{payment_code}/pay

고객이 상품권으로 결제합니다. 상품권 소유권이 가맹점으로 이전되고, 결제가 완료됩니다.

요청 파라미터
파라미터필수타입설명
gift_card_ids필수array결제할 상품권 ID 배열
message선택string결제 메시지 (최대 200자)
요청 예시
curl -X POST https://nexva.giftp2p.net/api/qr-pay/A1B2C3D4E5F6/pay \
  -H "Authorization: Bearer {TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"gift_card_ids": [100, 101]}'
응답 예시 200
{
  "success": true,
  "message": "결제가 완료되었습니다.",
  "payment": {
    "payment_code": "A1B2C3D4E5F6",
    "amount": 50000,
    "paid_amount": 50000,
    "merchant_name": "테스트 가맹점",
    "gift_count": 2
  }
}
결제 완료 시 등록된 콜백 URL로 qr_payment_completed 이벤트가 자동 전송됩니다. (최대 3회 재시도)
GET 결제 상태 조회
/api/qr-pay/{payment_code}/status

인증 없이 호출 가능합니다. 결제 상태 폴링에 사용하세요.

응답 예시 200
{
  "payment_code": "A1B2C3D4E5F6",
  "status": "completed",
  "amount": 50000,
  "paid_amount": 50000,
  "payer_name": "홍길동",
  "completed_at": "2026-04-06 15:05:30"
}
POST 결제 취소
/api/qr-pay/{payment_code}/cancel

pending 상태의 결제만 취소 가능합니다. 본인이 생성한 결제만 취소할 수 있습니다.

응답 예시 200
{
  "success": true,
  "message": "결제가 취소되었습니다."
}
GET 결제 목록 조회
/api/qr-pay/my-payments

본인이 생성한 QR 결제 목록을 조회합니다. 페이지네이션 지원.

쿼리 파라미터
파라미터설명
page페이지 번호 (기본 1)
status필터: pending, completed, cancelled, expired
응답 예시 200
{
  "data": [
    {
      "id": 42,
      "payment_code": "A1B2C3D4E5F6",
      "amount": 50000,
      "paid_amount": 50000,
      "description": "주문 #1234",
      "external_id": "ORDER-1234",
      "customer_id": "CUST-5678",
      "status": "completed",
      "payer_name": "홍길동",
      "payment_url": "https://nexva.giftp2p.net/pay?code=A1B2C3D4E5F6",
      "qr_image_url": "https://nexva.giftp2p.net/api/qr-pay/A1B2C3D4E5F6/qr.png",
      "completed_at": "2026-04-06 15:05:30",
      "expires_at": "2026-04-06 16:00:00",
      "created_at": "2026-04-06 15:00:00"
    }
  ],
  "meta": {
    "total": 1,
    "per_page": 20,
    "current_page": 1,
    "last_page": 1
  }
}
GET 내 상품권 조회
/api/qr-pay/my-cards

인증된 사용자가 보유한 상품권 목록을 조회합니다. 잔액 확인 용도로 사용합니다.

요청 예시
curl -X GET https://nexva.giftp2p.net/api/qr-pay/my-cards \
  -H "Authorization: Bearer {TOKEN}" \
  -H "Accept: application/json"
응답 예시 200
{
  "cards": [
    {
      "id": 100,
      "serial_no": "GC20260406001",
      "brand_name": "행복",
      "face_value": 50000,
      "created_at": "2026-04-01 10:00:00"
    },
    {
      "id": 101,
      "serial_no": "GC20260406002",
      "brand_name": "행복",
      "face_value": 30000,
      "created_at": "2026-04-02 14:30:00"
    }
  ],
  "total_count": 2,
  "total_value": 80000
}
owned 상태이며 만료되지 않은 상품권만 반환됩니다. total_value로 환전 가능한 총 잔액을 확인하세요.
POST 상품권 환전 신청
/api/qr-pay/redeem

가맹점이 보유 상품권을 금액 기준으로 환전 신청합니다. 시스템이 보유 상품권에서 자동으로 필요한 만큼 선택하며, 금액이 초과하는 상품권은 자동 분할(split) 처리하여 정확한 금액만 환전합니다.

처리 흐름 (2026-05-08 정책 변경):
① API 호출 → 환전 신청 (status=pending) 생성
② 가맹점이 /merchant/qr-pay/redemptions 페이지에서 승인 / 취소 처리
③ 승인 시 customer_id 회원에게 상품권 전달 + redeem_gift_completed 콜백
④ 취소 시 상품권 복원 + redeem_cancelled 콜백
⑤ 승인완료 후 원복 가능 (회원 미사용 상태일 때만) → redeem_reverted 콜백
API 환전 수수료: API 경로(/api/qr-pay/redeem)로 들어오는 환전은 수수료가 부과되지 않습니다. 액면가 그대로 처리됩니다.
요청 파라미터
파라미터필수타입설명
amount필수integer환전 금액 (최소 1,000원). 보유 상품권 총액 이내여야 합니다.
customer_id필수string받는 회원의 P2P login_id (최대 100자). 가맹점 승인 시 이 회원에게 상품권이 전달됩니다. 매칭되는 회원이 없으면 즉시 422 에러.
external_id선택string가맹점 주문번호 등 외부 식별자 (최대 100자). 콜백에 external_id로 그대로 반환됩니다.
memo선택string메모 (최대 500자).
요청 예시
curl -X POST https://nexva.giftp2p.net/api/qr-pay/redeem \
  -H "Authorization: Bearer {TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 150000,
    "customer_id": "skcoo22",
    "external_id": "ORDER-5678"
  }'
응답 예시 200
{
  "success": true,
  "message": "환전 신청이 접수되었습니다. 가맹점 페이지에서 승인하면 처리됩니다.",
  "request_no": "RDM260508ABCDEF",
  "status": "pending",
  "amount": 150000,
  "item_count": 2,
  "receiver": "skcoo22",
  "receiver_name": "홍길동"
}
실패 응답 — 받는 회원 미발견 422
{
  "success": false,
  "error": "받는 회원을 찾을 수 없습니다. customer_id 와 일치하는 P2P 회원이 없습니다.",
  "reason": "receiver_not_found",
  "customer_id": "skcoo22"
}

가맹점 콜백으로 redeem_failed 이벤트가 함께 발송됩니다.

자동 분할 예시

보유 상품권: 100,000원 × 2장 = 총 200,000원

환전 요청: 150,000원

처리 결과:

  • 100,000원 상품권 1장 → 전액 환전
  • 100,000원 상품권 1장 → 50,000원 분할 환전 + 50,000원 잔여 상품권 자동 생성
분할 권종: 잔여 상품권은 500,000 / 100,000 / 50,000 / 10,000원 단위로 자동 생성됩니다.
승인 / 취소는 가맹점 본인이 수행하며, 각 액션마다 등록된 콜백 URL로 이벤트가 발송됩니다.
웹훅 콜백

결제 완료 및 환전 완료 시 등록된 URL로 HTTP POST (application/json) 요청이 자동 전송됩니다.

재시도 정책: 콜백 전송 실패(HTTP 200 이외 응답 또는 타임아웃) 시 최대 3회 재시도합니다. (60초 간격)
QR 결제 완료 (qr_payment_completed)
{
  "event": "qr_payment_completed",
  "payment_code": "A1B2C3D4E5F6",
  "external_id": "ORDER-1234",
  "customer_id": "CUST-5678",
  "amount": 50000,
  "paid_amount": 50000,
  "description": "주문 #1234 결제",
  "payer": {
    "user_id": 42,
    "name": "홍길동"
  },
  "gift_card_count": 1,
  "completed_at": "2026-04-06 15:05:30"
}
환전 승인 — 회원에게 상품권 전달 완료 (redeem_gift_completed)

가맹점이 환전 신청을 승인하면 customer_id 회원에게 상품권이 즉시 전달되며 이 이벤트가 발송됩니다.

{
  "event": "redeem_gift_completed",
  "request_no": "RDM260508ABCDEF",
  "external_id": "ORDER-5678",
  "memo": "테스트 메모",
  "customer_id": "skcoo22",
  "amount": 100000,
  "item_count": 2,
  "receiver_id": 35,
  "receiver_name": "홍길동",
  "sender_id": 33,
  "sender_name": "야옹이",
  "completed_at": "2026-05-08T15:30:00+09:00"
}
환전 취소 (redeem_cancelled)

가맹점이 환전 신청을 취소(거절)하면 보유 상품권이 가맹점으로 복원되고 이 이벤트가 발송됩니다.

{
  "event": "redeem_cancelled",
  "request_no": "RDM260508ABCDEF",
  "external_id": "ORDER-5678",
  "memo": "테스트 메모",
  "customer_id": "skcoo22",
  "amount": 100000,
  "item_count": 2,
  "cancelled_at": "2026-05-08T15:35:00+09:00"
}
환전 원복 (redeem_reverted)

승인완료된 환전을 가맹점이 원복하면 회원에게 전달된 상품권이 가맹점으로 회수되며 이 이벤트가 발송됩니다.
주의: 회원이 이미 사용/판매/재선물한 상품권은 회수 불가합니다 (failed_count로 알림).

{
  "event": "redeem_reverted",
  "request_no": "RDM260508ABCDEF",
  "external_id": "ORDER-5678",
  "memo": "테스트 메모",
  "customer_id": "skcoo22",
  "amount": 100000,
  "item_count": 2,
  "recovered_count": 2,
  "failed_count": 0,
  "errors": [],
  "reverted_at": "2026-05-08T16:00:00+09:00"
}

아울러 카드별로 gift_recalled 이벤트도 발송됩니다(상품권 단위 상세).

환전 실패 — 받는 회원 미발견 (redeem_failed)

API 호출 시점에 customer_id가 매칭되지 않으면 환전이 생성되지 않고 이 이벤트가 발송됩니다.

{
  "event": "redeem_failed",
  "reason": "receiver_not_found",
  "message": "받는 회원을 찾을 수 없습니다.",
  "external_id": "ORDER-5678",
  "customer_id": "skcoo22",
  "amount": 100000,
  "memo": "테스트 메모",
  "failed_at": "2026-05-08T15:00:00+09:00"
}
콜백 수신 서버 요구사항
  • HTTPS URL 권장 (HTTP도 가능)
  • 타임아웃: 10초 이내 응답
  • 성공 시 HTTP 200 반환 필수
  • 200 이외 응답 또는 타임아웃 시 실패로 간주하고 재시도
연동 흐름
QR 결제 흐름
  1. 가맹점 서버: POST /api/qr-pay 호출 → payment_url, qr_image_url 수신
  2. 가맹점: qr_image_url로 QR 이미지 표시, 또는 payment_url을 직접 공유
  3. 고객: QR 스캔 → 결제 페이지에서 상품권 선택 후 결제
  4. 시스템: 상품권 소유권 이전 + 결제 상태 completed로 변경
  5. 시스템 → 가맹점: 콜백 URL로 qr_payment_completed 이벤트 전송
  6. 가맹점 서버: external_id로 주문 매칭, customer_id로 회원 매핑, 서비스 제공
상품권 환전 흐름 (2026-05-08 정책)
  1. 가맹점 서버: POST /api/qr-pay/redeem 호출 → 환전 신청 생성 (status=pending)
    • customer_id 가 매칭 안 되면 즉시 422 + redeem_failed 콜백 (환전 미생성)
  2. 가맹점: /merchant/qr-pay/redemptions 페이지에서 [승인] 또는 [취소] 클릭
    • 승인 시 2단계 confirm — 받는 회원 / 금액 / 장수 노출
    • 받는 회원 미매칭 시 승인 버튼 자동 비활성화
  3. 승인: 보유 상품권이 customer_id 회원에게 즉시 전달 → 콜백 redeem_gift_completed
  4. 취소: 보유 상품권 그대로 가맹점 잔존 → 콜백 redeem_cancelled
  5. 원복: 승인완료 후 회원이 미사용 상태일 때 [원복] 클릭 → 상품권 회수 → 콜백 redeem_reverted + 카드별 gift_recalled
관리자 개입 없음: 환전 처리는 가맹점 본인이 책임지고 결정합니다. 본사 관리자는 모니터링만 수행하며 직접 승인하지 않습니다.
수수료 면제: API 경로 환전은 항상 액면가 그대로 처리됩니다 (수수료 0).
external_id / customer_id 활용
필드용도예시
external_id거래(주문) 단위 식별자. QR 생성 시 전달 → 콜백에서 주문 매칭"ORDER-1234"
customer_id회원 단위 식별자. QR 생성/환전 시 전달 → 콜백에서 회원 매핑"USER-5678"

한 유저가 여러 가맹점에 소속되어 있어도, 건별로 customer_id를 전달하므로 정확한 매핑이 가능합니다.

샘플 코드

QR 결제 생성 → 상태 폴링 → 콜백 수신까지의 전체 연동 샘플입니다.

1. QR 결제 생성
curl -X POST https://nexva.giftp2p.net/api/qr-pay \
  -H "Authorization: Bearer {TOKEN}" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "amount": 50000,
    "description": "주문 #1234",
    "external_id": "ORDER-1234",
    "customer_id": "CUST-5678"
  }'
2. 결제 상태 조회
curl -X GET "https://nexva.giftp2p.net/api/qr-pay/{PAYMENT_CODE}/status" \
  -H "Accept: application/json"
3. 환전 신청
curl -X POST https://nexva.giftp2p.net/api/qr-pay/redeem \
  -H "Authorization: Bearer {TOKEN}" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "amount": 150000,
    "external_id": "REDEEM-001",
    "customer_id": "CUST-5678"
  }'
4. 결제 취소
curl -X POST "https://nexva.giftp2p.net/api/qr-pay/{PAYMENT_CODE}/cancel" \
  -H "Authorization: Bearer {TOKEN}" \
  -H "Accept: application/json"
API 헬퍼 클래스
<?php
/**
 * NEXVA QR 결제 API 클라이언트
 */
class NexvaQrPay
{
    private string $baseUrl = 'https://nexva.giftp2p.net/api/qr-pay';
    private string $token;

    public function __construct(string $token)
    {
        $this->token = $token;
    }

    /** QR 결제 생성 */
    public function createPayment(int $amount, array $options = []): array
    {
        return $this->request('POST', '/', array_merge([
            'amount' => $amount,
        ], $options));
    }

    /** 결제 상태 조회 */
    public function getStatus(string $paymentCode): array
    {
        return $this->request('GET', "/{$paymentCode}/status");
    }

    /** 결제 취소 */
    public function cancel(string $paymentCode): array
    {
        return $this->request('POST', "/{$paymentCode}/cancel");
    }

    /** 환전 신청 (금액 기반, 자동 분할) */
    public function redeem(int $amount, array $options = []): array
    {
        return $this->request('POST', '/redeem', array_merge([
            'amount' => $amount,
        ], $options));
    }

    /** 결제 목록 조회 */
    public function listPayments(int $page = 1, ?string $status = null): array
    {
        $query = ['page' => $page];
        if ($status) $query['status'] = $status;
        return $this->request('GET', '/my-payments?' . http_build_query($query));
    }

    /** 내 상품권 조회 */
    public function myCards(): array
    {
        return $this->request('GET', '/my-cards');
    }

    private function request(string $method, string $path, array $data = []): array
    {
        $ch = curl_init($this->baseUrl . $path);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . $this->token,
                'Content-Type: application/json',
                'Accept: application/json',
            ],
        ]);

        if ($method === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        $result = json_decode($response, true) ?? [];
        $result['_http_code'] = $httpCode;
        return $result;
    }
}

// ──────── 사용 예시 ────────

$api = new NexvaQrPay('YOUR_API_TOKEN');

// 1. QR 결제 생성
$payment = $api->createPayment(50000, [
    'description' => '주문 #1234',
    'external_id' => 'ORDER-1234',
    'customer_id' => 'CUST-5678',
]);
echo "결제코드: " . $payment['payment']['payment_code'] . "\n";
echo "결제 URL: " . $payment['payment']['payment_url'] . "\n";
echo "QR 이미지: " . $payment['payment']['qr_image_url'] . "\n";

// 2. 상태 폴링 (결제 완료 대기)
do {
    sleep(3);
    $status = $api->getStatus($payment['payment']['payment_code']);
} while ($status['status'] === 'pending');

echo "결제 상태: " . $status['status'] . "\n";

// 3. 환전 신청
$redeem = $api->redeem(150000, [
    'external_id' => 'REDEEM-001',
    'customer_id' => 'CUST-5678',
]);
echo "환전 요청번호: " . $redeem['request_no'] . "\n";
콜백 수신 (webhook receiver)
<?php
// callback.php — 콜백 수신 서버
$payload = json_decode(file_get_contents('php://input'), true);

if (!$payload || !isset($payload['event'])) {
    http_response_code(400);
    exit('Invalid payload');
}

// 로그 기록
file_put_contents('webhook.log', date('Y-m-d H:i:s') . ' ' . json_encode($payload) . "\n", FILE_APPEND);

switch ($payload['event']) {
    case 'qr_payment_completed':
        // QR 결제 완료 처리
        $externalId = $payload['external_id'];  // 주문번호
        $customerId = $payload['customer_id'];  // 회원 식별자
        $amount     = $payload['paid_amount'];  // 결제 금액

        // TODO: 주문 완료 처리, 포인트 적립 등
        // updateOrder($externalId, 'paid', $amount);
        break;

    case 'redeem_gift_completed':
        // 가맹점이 환전 신청을 승인 → customer 에게 상품권 전달 완료
        $externalId = $payload['external_id'];
        $customerId = $payload['customer_id'];
        $amount     = $payload['amount'];
        // TODO: 포인트 적립 등
        // creditBalance($customerId, $amount);
        break;

    case 'redeem_cancelled':
        // 가맹점이 환전 신청을 취소
        $externalId = $payload['external_id'];
        // TODO: 주문 취소 처리
        break;

    case 'redeem_reverted':
        // 승인된 환전이 원복됨 (전달된 상품권 회수)
        $externalId      = $payload['external_id'];
        $recoveredCount  = $payload['recovered_count'];
        $failedCount     = $payload['failed_count'];
        // TODO: 포인트 회수 처리
        break;

    case 'redeem_failed':
        // customer_id 미매칭으로 환전 실패
        $externalId = $payload['external_id'];
        // TODO: 알림 / 재시도 안내
        break;
}

// 반드시 200 응답 (그 외 응답은 재시도 대상)
http_response_code(200);
echo json_encode(['received' => true]);
API 헬퍼 모듈
// nexva-qrpay.js
const BASE_URL = 'https://nexva.giftp2p.net/api/qr-pay';

class NexvaQrPay {
  constructor(token) {
    this.token = token;
  }

  /** QR 결제 생성 */
  async createPayment(amount, options = {}) {
    return this.request('POST', '/', { amount, ...options });
  }

  /** 결제 상태 조회 */
  async getStatus(paymentCode) {
    return this.request('GET', `/${paymentCode}/status`);
  }

  /** 결제 취소 */
  async cancel(paymentCode) {
    return this.request('POST', `/${paymentCode}/cancel`);
  }

  /** 환전 신청 (금액 기반, 자동 분할) */
  async redeem(amount, options = {}) {
    return this.request('POST', '/redeem', { amount, ...options });
  }

  /** 결제 목록 조회 */
  async listPayments(page = 1, status = null) {
    const params = new URLSearchParams({ page });
    if (status) params.set('status', status);
    return this.request('GET', `/my-payments?${params}`);
  }

  /** 내 상품권 조회 */
  async myCards() {
    return this.request('GET', '/my-cards');
  }

  async request(method, path, data = null) {
    const options = {
      method,
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
    };
    if (data && method === 'POST') {
      options.body = JSON.stringify(data);
    }

    const res = await fetch(BASE_URL + path, options);
    const json = await res.json();
    json._http_code = res.status;
    return json;
  }
}

module.exports = NexvaQrPay;
사용 예시
const NexvaQrPay = require('./nexva-qrpay');
const api = new NexvaQrPay('YOUR_API_TOKEN');

(async () => {
  // 1. QR 결제 생성
  const payment = await api.createPayment(50000, {
    description: '주문 #1234',
    external_id: 'ORDER-1234',
    customer_id: 'CUST-5678',
  });
  console.log('결제코드:', payment.payment.payment_code);
  console.log('결제 URL:', payment.payment.payment_url);
  console.log('QR 이미지:', payment.payment.qr_image_url);

  // 2. 상태 폴링
  let status;
  do {
    await new Promise(r => setTimeout(r, 3000));
    status = await api.getStatus(payment.payment.payment_code);
  } while (status.status === 'pending');
  console.log('결제 상태:', status.status);

  // 3. 환전 신청
  const redeem = await api.redeem(150000, {
    external_id: 'REDEEM-001',
    customer_id: 'CUST-5678',
  });
  console.log('환전 요청번호:', redeem.request_no);
})();
콜백 수신 서버 (Express)
const express = require('express');
const app = express();
app.use(express.json());

app.post('/webhook/nexva', (req, res) => {
  const payload = req.body;
  console.log(`[${new Date().toISOString()}] ${payload.event}`, payload);

  switch (payload.event) {
    case 'qr_payment_completed':
      // QR 결제 완료
      const { external_id, customer_id, paid_amount } = payload;
      // TODO: 주문 완료 처리
      // await updateOrder(external_id, 'paid', paid_amount);
      break;

    case 'redeem_gift_completed':
      // 가맹점이 환전 신청을 승인 → customer 에게 상품권 전달 완료
      const { external_id: rid, customer_id: cid, amount } = payload;
      // TODO: 포인트 적립
      // await creditBalance(cid, amount);
      break;

    case 'redeem_cancelled':
      // 가맹점이 환전 신청을 취소
      // TODO: 주문 취소 처리
      break;

    case 'redeem_reverted':
      // 승인된 환전이 원복됨 (전달된 상품권 회수)
      const { recovered_count, failed_count } = payload;
      // TODO: 포인트 회수 처리
      break;

    case 'redeem_failed':
      // customer_id 미매칭으로 환전 실패
      // TODO: 알림 / 재시도 안내
      break;
  }

  // 반드시 200 응답
  res.json({ received: true });
});

app.listen(3000, () => console.log('Webhook server on :3000'));
에러 코드
HTTP 코드설명예시
401인증 실패토큰 미전송 또는 만료
404리소스 없음결제 코드 불일치
422처리 불가이미 완료/취소된 결제, 금액 부족, 만료 등
429요청 제한 초과Rate limit (결제 생성: 30회/분, 결제 실행: 10회/분)
에러 응답 형식
{
  "error": "이미 처리된 결제입니다."
}

밸리데이션 오류 시 errors 객체에 필드별 에러 메시지가 포함됩니다.

결제 상태값
pending결제 대기 중 (고객 결제 전)
completed결제 완료
cancelled가맹점에 의해 취소됨
expired만료 시간 초과