정기결제 연동하기


이 문서는 아임포트의 정기 결제 기능을 구현하는 방법을 설명합니다.카드를 등록하여 빌링키를 발급 받은 후 원하시는 정기 결제 기능를 다음과 같이 구현할 수 있습니다.
  • 즉시 정기 결제: 즉시 결제 요청 > 결제 예약 / 반복 결제 구현
  • 지연(delayed) 정기 결제: 결제 예약 / 반복 결제 구현
  • 이용할 때 마다 결제: 결제 요청 (매번 이용 후 혹은 원하는 시점에)
STEP1카드 등록 및 빌링키 발급받기
고객의 카드정보를 카드사에 전달하고 해당 카드에 대응하는 빌링키를 발급받습니다.

빌링키란?

구독형 정기결제, 종량제 과금결제 등 원하는 시점에 재결제를 진행할 수 있는 결제용 암호화 키 입니다. 가맹점이 고객의 카드정보를 소유할 수 없기 때문에 카드사로부터 해당 카드에 대응하는 빌링키 발급 받아 저장하고, 원하는 시점에 해당 빌링키로 결제를 청구할 수 있습니다.

customer_uid 속성

결제용 암호화 키인 빌링키는 카드사에서 발급이 된 후 지정하신 customer_uidunique key로 하여 아임포트 서버에 저장됩니다. 보안상의 이유로 서버는 빌링키에 직접 접근할 수 없기 때문에 빌링키와 대응하는 customer_uid를 활용해야 합니다.

만약 고객이 여러장의 카드를 등록하였다면, customer_uid를 카드 한 장에 대응하도록 값을 구성해야 합니다.

예: 고객의 unique key = gildong_0001, 카드 뒤 네자리 번호 = 1234 일 경우: customer_uid = gildong_0001_1234
빌링키는 PG사에 따라 다음 두 가지 방식을 활용하여 발급 받을 수 있습니다.
  • A. REST API: 나이스페이먼츠, JTNet, KCP, KG이니시스, 다우데이타(페이조아)
  • B. 일반결제창: JTNet, KG이니시스, 다날-신용카드, 다날-휴대폰, 모빌리언스, 페이코, KICC, KCP, 카카오페이, 네이버페이

A. REST API 활용하기

나이스페이먼츠, JTNet, KCP, 다우데이타(페이조아)

서버가 아임포트 REST API를 사용하여 아임포트 서버에 카드정보를 전달하면, 아임포트 서버가 PG사의 API를 호출하여 빌링키를 발급받습니다. 이 과정에서 카드정보는 기록되지 않습니다.

이 방식은 다음과 같은 특징이 있습니다.
  • 장점: 가맹점이 원하는 형태의 화면으로 카드정보 입력란을 커스터마이징할 수 있다.
  • 단점: 카드정보 전달과정의 보안프로세스를 구축해야하며, 개인정보 이용약관을 명시해야 한다.

PG사별 API 방식으로 빌링키 발급받기

PG사별 API 활용 방법은 PG사별 정기결제(빌링) 연동 가이드에서 각 API 방식 가이드를 참고하세요.
1카드 정보 입력받기
client-side

카드 정보를 입력하는 필드들을 다음과 같이 작성합니다. 요청 시 가맹점에 전달할 customer_uid를 저장 할 히든필드를 작성합니다. 법인카드(개인명의로 발급된 기명카드 제외)의 경우 birth 파라미터에 사업자번호 10자리를 입력하시면 됩니다.결제하기 버튼 클릭 시 입력값들과 customer_uid/subscription/issue-billing에 대해 POST요청이 호출되는 예제입니다.
  <form action="{빌링키 발급 요청을 받을 서비스 URL}", method="post">
  <!--예: https://www.myservice.com/subscription/issue-billing-->
    <div>
        <label for="card_number">카드 번호 XXXX-XXXX-XXXX-XXXX</label>
        <input id="card_number" type="text" name="card_number">
    </div>
    <div>
        <label for="expiry">카드 유효기간 YYYY-MM</label>
        <input id="expiry" type="text" name="expiry">
    </div>
    <div>
        <label for="birth">생년월일 YYMMDD</label>
        <input id="birth" type="text" name="birth">
    </div>
    <div>
        <label for="pwd_2digit">카드 비밀번호 앞 두자리 XX</label>
        <input id="pwd_2digit" type="text" name="pwd_2digit">
    </div>
    <input hidden type="text" value="gildong_0001_1234" name="customer_uid">
    <input type="submit" value="결제하기">
  </form>
2카드 정보 추출하기
server-side

카드 정보를 전달받을 API endpoint를 작성하고 요청에 담긴 카드 정보를 추출합니다.

/subscription/issue-billing에 대한 POST요청을 처리하는 API endpoint의 예제입니다.
  // "/subscription/issue-billing"에 대한 POST 요청을 처리
  app.post("/subscriptions/issue-billing", async (req, res) => {
    try {
      const {
        card_number, // 카드 번호
        expiry, // 카드 유효기간
        birth, // 생년월일
        pwd_2digit, // 카드 비밀번호 앞 두자리,
        customer_uid, // 카드(빌링키)와 1:1로 대응하는 값
      } = req.body; // req의 body에서 카드정보 추출
      ...
    } catch (e) {
      res.status(400).send(e);
    }
  });
3발급 요청 및 응답 처리하기
server-side

빌링키를 발급 받기 위해서 먼저 REST API access token을 발급받습니다.

발급받은 액세스 토큰(access token)과 customer_uid로 REST API(POST https://api.iamport.kr/subscribe/customers/${customer_uid})를 호출하여 빌링키 발급 요청을 하고 결과(code)값에 따라 응답을 반환하는 예제입니다.
    // "/subscription/issue-billing"에 대한 POST 요청을 처리
    app.post("/subscriptions/issue-billing", async (req, res) => {
      try {
        const {
          card_number, // 카드 번호
          expiry, // 카드 유효기간
          birth, // 생년월일
          pwd_2digit, // 카드 비밀번호 앞 두자리
          customer_uid, // 카드(빌링키)와 1:1로 대응하는 값
        } = req.body; // req의 body에서 카드정보 추출
        ...
        // 인증 토큰 발급 받기
        const getToken = await axios({
          url: "https://api.iamport.kr/users/getToken",
          method: "post", // POST method
          headers: { "Content-Type": "application/json" }, // "Content-Type": "application/json"
          data: {
            imp_key: "imp_apikey", // REST API 키
            imp_secret: "ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f" // REST API Secret
          }
        });
        const { access_token } = getToken.data.response; // 인증 토큰
        ...
        // 빌링키 발급 요청
        const issueBilling = await axios({
          url: \`https://api.iamport.kr/subscribe/customers/\${customer_uid}\`,
          method: "post",
          headers: { "Authorization": access_token }, // 인증 토큰 Authorization header에 추가
          data: {
            card_number, // 카드 번호
            expiry, // 카드 유효기간
            birth, // 생년월일
            pwd_2digit, // 카드 비밀번호 앞 두자리
          }
        });
        ...
        const { code, message } = issueBilling.data;
        if (code === 0) { // 빌링키 발급 성공
          res.send({ status: "success", message: "Billing has successfully issued" });
        } else { // 빌링키 발급 실패
          res.send({ status: "failed", message });
        }
      } catch (e) {
        res.status(400).send(e);
      }
    });

빌링키 발급과 결제 요청를 한번에 하기

키인결제 REST API /subscribe/payments/onetime를 사용하여 일회성 결제를 요청하거나 빌링키 발급과 최초 결제를 같이 요청할 수 있습니다.

B. 일반결제창 활용하기

JTNet, KG이니시스, 다날-신용카드, 다날-휴대폰, 모빌리언스, 페이코, KICC, KCP, 카카오페이, 네이버페이

PG사가 제공하는 일반 결제창에 고객이 카드정보를 입력하여 빌링키를 발급 받습니다.

이 방식은 다음과 같은 특징이 있습니다.
  • 장점: 카드정보가 서버 또는 아임포트의 서버를 거치지 않고 직접 PG사로 전달되기 때문에 데이터 및 통신구간 암호화 등의 추가 보안 프로세스가 없다.
  • 단점: PG사의 일반결제창을 통해 카드정보를 입력받기 때문에 웹브라우저를 통해서만 빌링키 발급이 이루어지며, 카드정보 입력란을 커스터마이징 할 수 없다.

PG사별 결제창 방식으로 빌링키 발급받기

결제창 호출 시, PG사에 따라 요구하는 param의 정책이 다르기 때문에 반드시 PG사별 정기결제(빌링) 연동 가이드에서 각 결제창 방식 가이드를 확인하세요.
1발급 요청하기
client-side

먼저 일반결제와 동일하게 아임포트 라이브러리를 추가하고 결제창을 실행 할 준비를 합니다. 필요한 결제 정보로 IMP.request_pay를 호출하면 빌링키가 발급됩니다. 단, 카드 등록을 위해서 다음 파라미터를 설정해야 합니다.

모바일 웹 환경

모바일 웹 상에서 KG이니시스, NHN KCP, JTNet, 그리고 KICC는 각 PG사의 웹사이트로 리다이렉트되어 결제가 되므로 일반결제의 모바일 웹 환경에서 연동하기 (특정 PG사에만 적용)를 참고하여 빌링키 발급을 요청합니다.
  // IMP.request_pay(param, callback) 호출
  IMP.request_pay({ // param 설정은 위에서 업급한 PG사별 연동 가이드를 참고하세요.
    customer_uid: "gildong_0001_1234", // 카드(빌링키)와 1:1로 대응하는 값
    /* ...생략... */
  }, function (rsp) { // callback
    if (rsp.success) {
      // 빌링키 발급 성공
    } else {
      // 빌링키 발급 실패
    }
  });
2발급 응답 처리하기
client-side server-side

클라이언트에서 빌링키가 성공적으로 발급되면 다음과 같이 customer_uid를 서버에 전달합니다.
  // IMP.request_pay(param, callback) 호출
  IMP.request_pay({ // param
    /* ...중략... */
  }, function (rsp) { // callback
    if (rsp.success) {
      // 빌링키 발급 성공
      // jQuery로 HTTP 요청
      jQuery.ajax({
        url: "{customer_uid를 받을 서비스 URL}", // 예: https://www.myservice.com/billings/
        method: "POST",
        headers: { "Content-Type": "application/json" },
        data: {
          customer_uid: "gildong_0001_1234", // 카드(빌링키)와 1:1로 대응하는 값
        }
      });
    } else {
      // 빌링키 발급 실패
    }
  });
서버에서는 클라이언트로부터 customer_uid를 전달받는 API endpoint를 생성합니다. 서버에서 해당 customer_uid를 사용하여 차후에 결제를 요청할 수 있습니다.
  // "/billings" 에 대한 POST 요청을 처리
  app.post("/billings", async (req, res) => {
    try {
      const { customer_uid } = req.body; // req body에서 customer_uid 추출
        ...
    } catch (e) {
      res.status(400).send(e);
    }
  });
STEP2결제 요청하기
server-side

카드를 등록하여 발급 받은 빌링키로 정기 결제 요청 API를 호출하여 결제 요청을 합니다.

먼저 결제하려는 카드의 customer_uid를 데이터베이스에서 가져온 뒤 REST API access token을 발급받습니다.

발급받은 액세스 토큰(access token)과 customer_uid 및 결제 정보로 REST API(POST https://api.iamport.kr/subscribe/payments/again)를 호출하여 다음과 같이 결제 요청을 합니다.
  // "/billings" 에 대한 POST 요청을 처리하는 controller
  app.post("/billings", async (req, res) => {
    try {
      const { customer_uid } = req.body; // req의 body에서 customer_uid 추출
      // 인증 토큰 발급 받기
      const getToken = await axios({
        url: "https://api.iamport.kr/users/getToken",
        method: "post", // POST method
        headers: { "Content-Type": "application/json" }, // "Content-Type": "application/json"
        data: {
          imp_key: "imp_apikey", // REST API 키
          imp_secret: "ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f" // REST API Secret
        }
      });
      const { access_token } = getToken.data.response; // 인증 토큰
      ...
      // 결제(재결제) 요청
      const paymentResult = await axios({
        url: \`https://api.iamport.kr/subscribe/payments/again\`,
        method: "post",
        headers: { "Authorization": access_token }, // 인증 토큰을 Authorization header에 추가
        data: {
          customer_uid,
          merchant_uid: "order_monthly_0001", // 새로 생성한 결제(재결제)용 주문 번호
          amount: 8900,
          name: "월간 이용권 정기결제"
        }
      });
      ...
      const { code, message } = paymentResult;
      if (code === 0) { // 카드사 통신에 성공(실제 승인 성공 여부는 추가 판단이 필요함)
        if ( paymentResult.status === "paid" ) { //카드 정상 승인
          res.send({ ... });
        } else { //카드 승인 실패 (예: 고객 카드 한도초과, 거래정지카드, 잔액부족 등)
          //paymentResult.status : failed 로 수신됨
          res.send({ ... });
        }
        res.send({ ... });
      } else { // 카드사 요청에 실패 (paymentResult is null)
        res.send({ ... });
      }
    } catch (e) {
      res.status(400).send(e);
    }
  });
STEP3결제 예약하기
server-side

정기결제 상품에 대해서 미래의 특정 시점에 결제를 시도하도록 예약할 수 있습니다.
1예약 등록하기
REST API(POST https://api.iamport.kr/subscribe/payments/schedule)를 호출하여 결제를 예약합니다. 하나의 customer_uid에 대해서 복수의 예약을 설정할 수 있습니다.
  // 결제 예약
  axios({
    url: \`https://api.iamport.kr/subscribe/payments/schedule\`,
    method: "post",
    headers: { "Authorization": access_token }, // 인증 토큰 Authorization header에 추가
    data: {
      customer_uid: "gildong_0001_1234", // 카드(빌링키)와 1:1로 대응하는 값
      schedules: [
        {
          merchant_uid: "order_monthly_0001", // 주문 번호
          schedule_at: 1519862400, // 결제 시도 시각 in Unix Time Stamp. 예: 다음 달 1일
          amount: 8900,
          name: "월간 이용권 정기결제",
          buyer_name: "홍길동",
          buyer_tel: "01012345678",
          buyer_email: "gildong@gmail.com"
        }
      ]
    }
  });
2결제 결과 저장하기
server-side

예약한 시간에 결제가 시도되면 Webhook 이벤트가 발생하여 지정한 서버의 callback URL로 결제 번호(imp_uid)와 주문 번호(merchant_uid)가 전달됩니다.

아임포트 Webhook

아임포트 Webhook의 개념과 callback URL을 설정하는 방법은 아임포트 Webhook 문서를 참고하세요.
Webhook 이벤트를 처리할 endpoint를 다음과 같이 작성하여 결제 정보를 조회하고 검증하여 저장합니다.
    // "/iamport-callback/schedule"에 대한 POST 요청을 처리
    app.post("/iamport-callback/schedule", async (req, res) => {
      try {
        const { imp_uid, merchant_uid } = req.body;
        // 액세스 토큰(access token) 발급 받기
        const getToken = await axios({
          url: "https://api.iamport.kr/users/getToken",
          method: "post", // POST method
          headers: { "Content-Type": "application/json" }, // "Content-Type": "application/json"
          data: {
            imp_key: "imp_apikey", // REST API 키
            imp_secret: "ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f" // REST API Secret
          }
        });
        const { access_token } = getToken.data.response; // 인증 토큰
        // imp_uid로 아임포트 서버에서 결제 정보 조회
        const getPaymentData = await axios({
          url: \`https://api.iamport.kr/payments/\${imp_uid}\`, // imp_uid 전달
          method: "get", // GET method
          headers: { "Authorization": access_token } // 인증 토큰 Authorization header에 추가
        });
        const paymentData = getPaymentData.data.response; // 조회한 결제 정보
        const { status } = paymentData;
        if (status === "paid) { // 결제 성공적으로 완료
          // DB에 결제 정보 저장
          await Orders.findByIdAndUpdate(merchant_uid, { $set: paymentData }); // Mongoose
          ...
        } else {
          // 재결제 시도
        }
      } catch (e) {
        res.status(400).send(e);
      }
    });
STEP4반복 결제 구현하기
server-side

다음과 같이 서버에서 아임포트 서버에 결제 예약 요청을 하고, 예약한 시간에 결제가 시도되면 지정된 callback URL을 통해서 서버에 알리는 과정을 반복적으로 수행하여 반복 결제를 구현할 수 있습니다. 해당 callback URL이 호출되면 서버에서 결제 후 처리를 수행하는 과정에서 다음 결제를 예약합니다.예약된 결제가 시도되었을 때 발생하는 webhook 이벤트를 처리하는 로직에서 예약된 결제가 정상적으로 완료되고 결제 내역이 저장되면 다음 결제를 예약하는 예제입니다.
    // "/iamport-callback/schedule"에 대한 POST 요청을 처리
    app.post("/iamport-callback/schedule", async (req, res) => {
      try {
        const { imp_uid, merchant_uid } = req.body;
        // 액세스 토큰(access token) 발급 받기
        /* ...중략 ... */
        // imp_uid로 아임포트 서버에서 결제 정보 조회
        /* ...중략 ... */
        const paymentData = getPaymentData.data.response; // 조회한 결제 정보
        const { status } = paymentData;
        if (status === "paid) { // 결제 성공적으로 완료
          // DB에 결제 정보 저장
          await Orders.findByIdAndUpdate(merchant_uid, { $set: paymentData }); // Mongoose
          ...
          // 새로운 결제 예약
          axios({
            url: "{결제예약을 받을 서비스 URL}", // 예: https://api.iamport.kr/subscribe/payments/schedule
            method: "post",
            headers: { "Authorization": access_token }, // 인증 토큰 Authorization header에 추가
            data: {
              customer_uid: "gildong_0001_1234", // 카드(빌링키)와 1:1로 대응하는 값
              schedules: [
                {
                  merchant_uid: "order_monthly_0001", // 주문 번호
                  schedule_at: 1519516800, // 결제 시도 시각 in Unix Time Stamp. 예: 다음 달 1일
                  amount: 8900,
                  name: "월간 이용권 정기결제",
                  ...
                }
              ]
            }
          });
        } else {
          // 재결제 시도
        }
      } catch (e) {
        res.status(400).send(e);
      }
    });