💸웹훅 연동하기
웹훅 연동을 통해 정확한 결제 결과를 처리할 수 있습니다.
포트원 서버에 저장된 결제 정보를 가맹점 서버에 동기화하고 네트워크 불안정성을 보완하는 방법을 설명합니다.
웹훅(Webhook) 이란?
특정 이벤트가 발생하였을 때 타 서비스나 응용프로그램으로 알림을 보내는 기능입니다.
웹훅 제공자는 특정 이벤트가 발생하면 HTTP POST
요청을 생성하여 등록된 callback URL로 이벤트 정보을 보냅니다. 주기적으로 데이터를 폴링(polling)하지 않고 원하는 이벤트에 대한 정보만 수신할 수 있어 웹훅은 리소스나 통신측면에서 효율적입니다.
웹훅 연동은 필수인가요?
웹훅 연동은 필수적입니다. Wi-Fi 연결 끊김, 브라우저 자동 새로고침 등의 이유로 클라이언트에서 결제 완료에 대한 응답을 받지 못하는 경우가 발생할 수 있습니다. 이런 경우를 대비해서 포트원 서버에서 가맹점 서버로 웹훅 이벤트를 발송하여 누락없이 결제 정보를 동기화할 수 있도록 합니다.
웹훅 발생 이벤트
포트원 웹훅은 다음과 같은 이벤트에 발생됩니다.
결제(예약 결제 포함)가 승인되었을 때(모든 결제 수단) - (status :
paid
)가상계좌가 발급되었을 때 - (status :
virtual_account_issued
)가상계좌에 결제 금액이 입금되었을 때 - (status :
paid
)관리자 콘솔에서 결제 취소되었을 때 - (status :
cancelled
)결제(예약 결제 포함)가 실패했을 때(status:
failed
)
웹훅 수신 설정
웹훅수신을 위한 URL 설정은 두가지 형태로 지원됩니다.
1. 관리자콘솔 설정

웹훅을 통해 결제 정보를 통보받을 URL을 설정하려면 포트원 관리자 콘솔 내 결제연동->실연동관리 탭을 선택합니다. Endpoint URL
항목에 웹훅으로 전송될 데이터를 수신할 URL주소를 기재합니다.
Content-Type 은 application/json
또는 application/x-www-form-urlencoded
으로 지정할 수 있습니다.
호출 테스트 버튼을 통해 올바른 URL과 Content-Type을 지정했는지 테스트할 수 있습니다.
2. SDK 파라미터 설정
포트원 SDK의 PortOne.requestPayment()
함수 파라미터 중 noticeUrls
를 통해 관리자콘솔에서 설정한 웹훅 수신 URL을 덮어쓸 수 있습니다.
PortOne.requestPayment({
/* 객체 생략 */
noticeUrls: ["https://수신할-웹훅-URL"],
});
웹훅 POST
요청의 본문은 다음의 정보를 포함합니다. 가맹점 서버는 아래 정보를 수신하고 포트원 서버에서 결제 정보를 조회하여 검증 및 저장할 수 있습니다.
tx_id
: 결제 트랜잭션 IDpayment_id
: 주문 IDstatus
: 결제 상태
// bodyParser 등을 통해 body의 JSON 데이터를 파싱할 수 있는지 확인해주세요.
// 관리자콘솔에서 설정한 Content-Type에 따라 다른 파서를 적용해야 할 수 있습니다.
app.use(bodyParser.json());
// POST 요청을 받는 /portone-webhook
app.post("/portone-webhook", async (req, res) => {
try {
const { tx_id, payment_id } = req.body;
// 1. 포트원 API를 사용하기 위한 액세스 토큰 발급 받기
const signinResponse = await axios({
url: "https://api.portone.io/v2/signin/api-key",
method: "post",
headers: { "Content-Type": "application/json" },
data: {
api_key: PORTONE_API_KEY, // 포트원 API Key
},
});
const { access_token } = signinResponse.data;
// 2. 포트원 결제내역 단건조회 API 호출
const paymentResponse = await axios({
url: `https://api.portone.io/v2/payments/${payment_id}`,
method: "get",
// 1번에서 발급받은 액세스 토큰을 Bearer 형식에 맞게 넣어주세요.
headers: { "Authorization": "Bearer " + access_token },
});
const { payment: { id, transactions } } = paymentResponse.data;
// 대표 트랜잭션(승인된 트랜잭션)을 선택합니다.
const transaction = transactions.find(tx => tx.is_primary === true);
// 3. 가맹점 내부 주문 데이터의 가격과 실제 지불된 금액을 비교합니다.
const order = await OrderService.findById(id);
if (order.amount === transaction.amount.total) {
switch (status) {
case "VIRTUAL_ACCOUNT_ISSUED": {
const { virtual_account } = transaction.payment_method_detail;
// 가상 계좌가 발급된 상태입니다.
// 계좌 정보(virtual_account)를 이용해 원하는 로직을 구성하세요.
break;
}
case "PAID": {
// 모든 금액을 지불했습니다! 완료 시 원하는 로직을 구성하세요.
break;
}
}
} else {
// 결제 금액이 불일치하여 위/변조 시도가 의심됩니다.
}
} catch (e) {
// 결제 검증에 실패했습니다.
res.status(400).send(e);
}
});
Last updated
Was this helpful?