개발자센터

3. 결제검증 API 구현하기 (서버)

클라이언트에서 결제를 구현하기 전 결제검증 API를 구현합니다.

예시를 위해 경로가 /payments/complete인 결제 성공 여부 확인 API를 구현합니다.

이 API는 내부적으로 포트원의 결제내역 단건조회 API를 호출하며 크게 3단계에 따라 정상적으로 결제가 이루어졌는지를 파악합니다.

  1. 포트원 API를 사용하기 위한 액세스 토큰 발급 받기 #링크
  2. 포트원 결제내역 단건조회 API 호출 #링크
  3. 가맹점 내부 주문 데이터와 지불된 금액 비교

가맹점의 DB에 저장된 값과 포트원에 저장된 값을 비교합니다. 검증이 성공하면 결제정보를 데이터베이스에 저장한 뒤 결제 상태(status)에 따라 알맞은 응답을 반환하고 실패 시 에러 메세지를 출력합니다.

// bodyParser 등을 통해 body의 JSON 데이터를 파싱할 수 있는지 확인해주세요.
app.use(bodyParser.json());
// POST 요청을 받는 /payments/complete
app.post("/payments/complete", async (req, res) => {
  try {
    // 요청의 body로 SDK의 응답 중 txId와 paymentId가 오기를 기대합니다.
    const { txId, paymentId } = 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/${paymentId}`,
      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);
  }
});