p2p 시스템 가맹점 연동 가이드
https://nexva.giftp2p.net모든 인증 API 요청에는 Bearer 토큰이 필요합니다.
Authorization: Bearer {YOUR_API_TOKEN}
Content-Type: application/json
Accept: application/json
GET /api/qr-pay/{code}, GET /api/qr-pay/{code}/status, GET /api/qr-pay/{code}/qr.png)는 토큰 없이 호출 가능합니다.
/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
}'
{
"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"
}
}
<img src="...">로 바로 사용 가능합니다.
/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 결제">
Content-Type: image/png, 캐시: 1시간 (Cache-Control: public, max-age=3600)
/api/qr-pay/{payment_code}
결제 정보를 조회합니다. 인증 없이 호출 가능합니다. 인증된 유저의 경우 보유 상품권 목록도 포함됩니다.
{
"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는 인증된 유저 + 결제 대기 상태일 때만 포함됩니다.
/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]}'
{
"success": true,
"message": "결제가 완료되었습니다.",
"payment": {
"payment_code": "A1B2C3D4E5F6",
"amount": 50000,
"paid_amount": 50000,
"merchant_name": "테스트 가맹점",
"gift_count": 2
}
}
qr_payment_completed 이벤트가 자동 전송됩니다. (최대 3회 재시도)
/api/qr-pay/{payment_code}/status
인증 없이 호출 가능합니다. 결제 상태 폴링에 사용하세요.
{
"payment_code": "A1B2C3D4E5F6",
"status": "completed",
"amount": 50000,
"paid_amount": 50000,
"payer_name": "홍길동",
"completed_at": "2026-04-06 15:05:30"
}
/api/qr-pay/{payment_code}/cancel
pending 상태의 결제만 취소 가능합니다. 본인이 생성한 결제만 취소할 수 있습니다.
{
"success": true,
"message": "결제가 취소되었습니다."
}
/api/qr-pay/my-payments
본인이 생성한 QR 결제 목록을 조회합니다. 페이지네이션 지원.
| 파라미터 | 설명 |
|---|---|
page | 페이지 번호 (기본 1) |
status | 필터: pending, completed, cancelled, expired |
{
"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
}
}
/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"
{
"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로 환전 가능한 총 잔액을 확인하세요.
/api/qr-pay/redeem
가맹점이 보유 상품권을 금액 기준으로 환전 신청합니다. 시스템이 보유 상품권에서 자동으로 필요한 만큼 선택하며, 금액이 초과하는 상품권은 자동 분할(split) 처리하여 정확한 금액만 환전합니다.
pending) 생성/merchant/qr-pay/redemptions 페이지에서 승인 / 취소 처리customer_id 회원에게 상품권 전달 + redeem_gift_completed 콜백redeem_cancelled 콜백redeem_reverted 콜백
/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"
}'
{
"success": true,
"message": "환전 신청이 접수되었습니다. 가맹점 페이지에서 승인하면 처리됩니다.",
"request_no": "RDM260508ABCDEF",
"status": "pending",
"amount": 150000,
"item_count": 2,
"receiver": "skcoo22",
"receiver_name": "홍길동"
}
{
"success": false,
"error": "받는 회원을 찾을 수 없습니다. customer_id 와 일치하는 P2P 회원이 없습니다.",
"reason": "receiver_not_found",
"customer_id": "skcoo22"
}
가맹점 콜백으로 redeem_failed 이벤트가 함께 발송됩니다.
보유 상품권: 100,000원 × 2장 = 총 200,000원
환전 요청: 150,000원
처리 결과:
결제 완료 및 환전 완료 시 등록된 URL로 HTTP POST (application/json) 요청이 자동 전송됩니다.
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"
}
HTTP 200 반환 필수POST /api/qr-pay 호출 → payment_url, qr_image_url 수신qr_image_url로 QR 이미지 표시, 또는 payment_url을 직접 공유completed로 변경qr_payment_completed 이벤트 전송external_id로 주문 매칭, customer_id로 회원 매핑, 서비스 제공POST /api/qr-pay/redeem 호출 → 환전 신청 생성 (status=pending)
customer_id 가 매칭 안 되면 즉시 422 + redeem_failed 콜백 (환전 미생성)/merchant/qr-pay/redemptions 페이지에서 [승인] 또는 [취소] 클릭
customer_id 회원에게 즉시 전달 → 콜백 redeem_gift_completedredeem_cancelledredeem_reverted + 카드별 gift_recalled| 필드 | 용도 | 예시 |
|---|---|---|
external_id | 거래(주문) 단위 식별자. QR 생성 시 전달 → 콜백에서 주문 매칭 | "ORDER-1234" |
customer_id | 회원 단위 식별자. QR 생성/환전 시 전달 → 콜백에서 회원 매핑 | "USER-5678" |
한 유저가 여러 가맹점에 소속되어 있어도, 건별로 customer_id를 전달하므로 정확한 매핑이 가능합니다.
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"
}'
curl -X GET "https://nexva.giftp2p.net/api/qr-pay/{PAYMENT_CODE}/status" \
-H "Accept: application/json"
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"
}'
curl -X POST "https://nexva.giftp2p.net/api/qr-pay/{PAYMENT_CODE}/cancel" \
-H "Authorization: Bearer {TOKEN}" \
-H "Accept: application/json"
<?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";
<?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]);
// 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);
})();
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 | 만료 시간 초과 |