본인인증

인증 정보 조회·검증

인증 완료 판단은 서버 조회값으로만 해요.

이 문서는 백엔드에서 receipt_id로 본인인증 정보를 조회하고, 완료 여부를 검증하고, 필요한 값만 저장하는 방법​​을 설명해요.

인증창이 닫혔다는 것과 인증이 성공했다는 것은 다른 이야기예요. 클라이언트가 보낸 done 이벤트는 신호일 뿐이고, 실제 판단은 서버가 조회한 status 값으로 해요.

핵심 요약

  • GET /certificate/{receipt_id}로 인증 내역을 조회해요. 인증 방식은 결제 API와 같은 Basic Auth예요.
  • status12(본인인증완료) 일 때만 인증 완료로 처리해요. 다른 값은 전부 미완료예요.
  • 조회는 승인 완료 후 30분 안에 해야 해요. 30분이 지나면 조회 API가 AUTH_EXPIRED로 차단돼요.
  • authenticate_data.uniqueCI(연계정보), authenticate_data.diDI(중복가입 확인정보)예요. 우리 서비스 안의 중복 가입 방지에는 di를 써요.
  • 클라이언트가 보낸 이름·전화번호는 저장하지 않아요. 반드시 조회 응답값을 저장해요.

API 정보

GEThttps://api.bootpay.co.kr/v2/certificate/{receipt_id}Basic Auth

인증 헤더는 API 인증과 같은 Basic Auth(client_key:secret_key) 방식이고, 서버 SDK에 키를 설정하면 자동으로 처리돼요.

조회·검증 코드

import { Bootpay } from '@bootpay/backend-js'

Bootpay.setConfiguration({
    client_key: '[ Client Key ]',
    secret_key: '[ Secret Key ]'
})

app.post('/auth/certificate', async (req, res) => {
    const { receipt_id } = req.body

    const certificate = await Bootpay.certificate(receipt_id)

    // status 12(본인인증완료)일 때만 인증 완료로 처리한다.
    if (certificate.status !== 12) {
        return res.status(400).json({ message: '본인인증이 완료되지 않았습니다.' })
    }

    // 조회 응답값만 저장한다. 클라이언트가 보낸 값은 신뢰하지 않는다.
    // unique = CI(기관 공통), di = DI(가맹점별)
    const data = certificate.authenticate_data
    await saveVerifiedUser({
        name: data.name,
        birth: data.birth,
        ci: data.unique,
        di: data.di
    })
    return res.json({ verified: true })
})javascript

응답

{
  "receipt_id": "6244f60c1fc19202e42e8c4e",
  "authentication_id": "auth_1718000000000",
  "gateway_url": "https://gw.bootpay.co.kr",
  "metadata": {},
  "pg": "다날",
  "method": "인증",
  "method_symbol": "auth",
  "method_origin": "인증",
  "method_origin_symbol": "auth",
  "status": 12,
  "status_locale": "본인인증완료",
  "requested_at": "2026-06-11T09:30:01+09:00",
  "authenticated_at": "2026-06-11T09:30:29+09:00",
  "authenticate_data": {
    "name": "홍길동",
    "phone": "01012345678",
    "birth": "19900101",
    "gender": 1,
    "foreigner": "0",
    "carrier": "SKT",
    "unique": "[ CI 값 ]",
    "di": "[ DI 값 ]",
    "tid": "danal_tid_..."
  }
}json
필드 타입 필수 설명
receipt_id String 필수 부트페이 인증 거래 ID
authentication_id String 필수 요청 시 보낸 가맹점 고유 인증번호
gateway_url String 필수 인증을 처리한 게이트웨이 URL
metadata Object 선택 요청 시 보낸 커스텀 데이터. 없으면 {}
pg String 필수 인증을 처리한 PG
method String 필수 인증 수단. 본인인증 건은 인증
method_symbol String 선택 인증 수단 코드. 본인인증 건은 auth
method_origin String 선택 PG가 내려준 원본 수단명. 본인인증 건은 인증
method_origin_symbol String 선택 원본 수단 코드. 본인인증 건은 auth
status Integer 필수 인증 상태. 12(본인인증완료)일 때만 완료로 처리해요
status_locale String 선택 상태의 한글 표기. 한글 라벨이 없는 상태(50·51·-50·-12)에서는 키 자체가 빠져요
requested_at Datetime 필수 인증 요청 시각
authenticated_at Datetime 선택 인증 완료 시각. 미완료 건은 비어 있어요
cancelled_at Datetime 선택 취소 시각. 값이 없으면 키가 빠져요
authenticate_data Object 필수 인증된 사용자 정보
name String 선택 이름
phone String 선택 휴대폰 번호
birth String 선택 생년월일. PG 원본 형식 그대로 전달돼요 (예: 다날 19900101)
gender Integer 선택 성별 (1: 남자, 0: 여자)
foreigner - 선택 내·외국인 구분값. PG마다 타입이 달라요 (다날은 원본 문자열 "0"/"1", KCP는 Integer 0/1)
carrier String 선택 통신사 (예: SKT)
unique String 선택 CI(연계정보). 기관 공통 식별값이라 어느 가맹점에서 인증해도 같은 값이에요
di String 선택 DI(중복가입 확인정보). 가맹점별 식별값이라 중복 가입 방지에 써요
tid String 필수 PG 거래 ID
status 값

12가 본인인증완료예요. 결제 상태값과 같은 체계를 쓰기 때문에 전체 목록은 결제 조회의 상태표에서 확인할 수 있어요. 본인인증 흐름에서는 12 외의 값을 모두 미완료로 처리하면 돼요.

foreigner는 PG마다 타입이 달라요

foreigner는 PG가 내려주는 내·외국인 구분값이에요. 다날은 PG 원본 문자열("0" / "1")이 그대로 전달되고, KCP는 Integer(0 / 1)로 변환돼요. 서버 분기 조건으로 쓴다면 타입까지 함께 비교하거나 문자열로 정규화한 뒤 판단해요.

인증 정보 저장 설계

30분 안에 저장해야 해요

조회 API는 승인 완료 시각(authenticated_at)부터 30분이 지나면 AUTH_EXPIRED로 차단​​돼요. 그래서 "나중에 필요하면 receipt_id로 다시 조회한다"는 설계는 동작하지 않아요. 인증 직후 조회해서 필요한 값만 내 DB에 저장해요.

30분이 지난 뒤 조회하면 AUTH_EXPIRED 에러가 반환되고, 사용자는 처음부터 다시 인증해야 해요.

CI와 DI를 구분해서 써요

authenticate_data에는 식별값이 두 개 들어와요. 이름이 헷갈리기 쉬우니 용도를 나눠서 저장해요.

필드 정체 범위 주 용도
unique CI(연계정보) 기관 공통. 어느 가맹점에서 인증해도 같은 값 동일인 식별, 타 서비스 연계
di DI(중복가입 확인정보) 가맹점(사이트)별. 같은 사람이라도 가맹점마다 다른 값 우리 서비스 안의 중복 가입 방지
  • 휴대폰 번호가 바뀌어도 DI가 같으면 우리 서비스에서 같은 사람​​이에요. 번호나 이름으로 동일인을 판단하면 안 돼요.
  • 회원 테이블에 DI를 저장해 두고, 신규 가입 시 같은 DI가 이미 있으면 중복 가입으로 처리해요.
  • CI·DI 모두 개인 식별정보예요. 평문 노출을 피하고 암호화 저장하며, 조회용으로만 쓰도록 접근을 제한해요.
// 중복 가입 방지 예시
const certificate = await Bootpay.certificate(receipt_id)
if (certificate.status !== 12) throw new Error('본인인증 미완료')

const { unique: ci, di, name, birth } = certificate.authenticate_data
const existing = await User.findByDi(di)   // 중복 가입 판단은 DI로

if (existing) {
    // 이미 가입된 사람 — 아이디 찾기·계정 통합 안내로 유도한다.
    return { duplicated: true, maskedEmail: mask(existing.email) }
}

await User.create({ ci, di, name, birth })javascript

저장은 최소한으로

개인정보 최소 수집

authenticate_data에는 이름·생년월일·휴대폰 번호 같은 개인정보가 들어 있어요. 서비스에 실제로 필요한 값만 저장하고, 나머지는 저장하지 않아요.

  • 성인 인증만 필요하다면 → 생년월일 대신 성인 여부(boolean) 만 저장하는 것도 방법이에요.
  • 중복 가입 방지만 필요하다면 → di(DI)만 저장하면 충분해요.
  • 저장한 항목은 개인정보 처리방침의 수집 항목·보관 기간과 일치시켜요.

에러 코드

코드 상황 처리
AUTH_NOT_FOUND (2407) 인증 정보를 찾지 못했어요 receipt_id가 올바른지 확인해요
AUTH_NOT_CONFIRMED (2408) 아직 승인(status: 12)되지 않은 건이에요 사용자 인증 완료 후 다시 조회해요
AUTH_EXPIRED (2409) 승인 완료 후 30분이 지나 조회할 수 없어요 인증을 처음부터 다시 요청해요
RC_NOT_AUTH (2064) 본인인증 건이 아니에요 (결제·정기결제 영수증) 본인인증으로 발급된 receipt_id인지 확인해요
API_ONLY_SELLER (600) 판매점 계정만 호출할 수 있어요 연동키가 판매점 프로젝트의 것인지 확인해요
APP_KEY_CHAIN_SESSION_INVALID (1225) 연동키 세션이 해당 인증 건과 맞지 않아요 인증을 요청할 때 쓴 것과 같은 프로젝트 연동키로 조회해요

전체 목록은 에러 코드표에서 확인해요.

다음 단계

  1. 인증창을 띄우는 코드를 보려면 → 인증창 연동
  2. 인증 화면을 직접 만들려면 → 인증창 없이 연동
  3. 서버 조회·검증의 공통 패턴을 보려면 → 결제 조회