이 문서는 부트페이 인증창을 띄우지 않고, 내가 만든 화면에서 본인인증을 진행하는 방법을 설명해요. 사용자 정보를 내 서버가 받아 인증을 요청하면 사용자 휴대폰으로 인증번호(OTP)가 전송되고, 사용자가 입력한 번호로 승인해요.
인증창 방식은 통신사 약관·UI가 바뀌어도 자동으로 대응되고, 주민등록번호 앞자리 같은 민감 정보를 내 서버가 직접 받지 않아도 돼요. 인증 화면 디자인을 반드시 직접 통제해야 하는 경우에만 이 방식을 선택해요. → 인증창 연동
전체 흐름
| 단계 | API | 설명 |
|---|---|---|
| ① 인증 요청 | POST /request/authentication |
이름·생년월일·통신사·휴대폰 번호로 인증 요청. 인증번호 발송 |
| ② 인증 승인 | POST /authenticate/confirm |
사용자가 입력한 OTP로 인증 승인 |
| ③ 인증번호 재전송 | POST /authenticate/realarm |
인증번호를 받지 못한 경우 재전송 |
| ④ 인증 정보 조회 | GET /certificate/{receipt_id} |
승인 응답 외에 다시 확인이 필요할 때 → 인증 정보 조회·검증 |
① 인증 요청
https://api.bootpay.co.kr/v2/request/authenticationBasic Auth| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
authentication_id |
String | 필수 | 가맹점 고유 인증번호. 유니크한 값으로 생성하고 DB에 저장해요 |
pg |
String | 필수 | 인증 PG. 인증창 없는 REST 방식은 다날만 지원해요 |
method |
String | 필수 | 본인인증 |
username |
String | 필수 | 사용자 이름 |
identity_no |
String | 필수 | 생년월일 6자리(YYMMDD) + 주민등록번호 뒤 첫 1자리 = 항상 7자리 (예: 9001011). 7자 미만이면 PG 호출 전에 거부돼요 |
carrier |
String | 필수 | 통신사. SKT / KT / LGT / SKT_MVNO / KT_MVNO / LGT_MVNO 6개 값만 허용돼요 (_MVNO는 알뜰폰) |
phone |
String | 필수 | 휴대폰 번호 (하이픈 없이) |
client_ip |
String | 선택 | 인증을 요청한 사용자의 IP. 생략하면 API를 호출한 서버의 IP가 대신 쓰여요 |
order_name |
String | 필수 | 인증 요청명 (예: "회원가입 본인인증") |
authenticate_type |
String | 선택 | sms(문자 인증) 또는 pass(PASS 앱 인증). 미입력 시 sms |
site_url |
String | 선택 | 본인인증을 진행하는 사이트 URL 또는 앱 이름. 부트페이가 강제하지는 않지만 다날 인증창 표기값(CPTITLE)으로 쓰이므로 채우는 것을 권장해요 |
user |
Object | 선택 | 추가 사용자 정보 |
extra |
Object | 선택 | 추가 옵션 |
metadata |
Object | 선택 | 전달할 추가 데이터. 결과에 그대로 반환돼요 |
client_ip를 생략하면 API를 호출한 서버의 IP가 대신 쓰여요. 이상 거래 판단에 쓰이는 값이므로 사용자 브라우저·앱의 IP를 넘기는 것을 권장해요. 프록시·로드밸런서 뒤에 있다면 X-Forwarded-For 헤더의 첫 번째 값을 써요.
import { Bootpay } from '@bootpay/backend-js'
Bootpay.setConfiguration({
client_key: '[ Client Key ]',
secret_key: '[ Secret Key ]'
})
// 사용자 휴대폰으로 인증번호가 전송된다.
const requested = await Bootpay.requestAuthentication({
authentication_id: 'auth_' + Date.now(),
pg: '다날',
method: '본인인증',
order_name: '회원가입 본인인증',
authenticate_type: 'sms',
username: '홍길동',
identity_no: '9001011', // 생년월일 6자리 + 주민등록번호 뒤 첫 1자리 = 7자리
carrier: 'SKT',
phone: '01012345678',
client_ip: req.headers['x-forwarded-for']?.split(',')[0] ?? req.socket.remoteAddress
})
// receipt_id를 세션·DB에 보관했다가 승인 단계에서 사용한다.
console.log(requested.receipt_id)javascriptfrom bootpay_backend import BootpayBackend
bootpay = BootpayBackend(client_key='[ Client Key ]', secret_key='[ Secret Key ]')
# 사용자 휴대폰으로 인증번호가 전송된다.
requested = bootpay.request_authentication(
authentication_id=f'auth_{int(time.time())}',
pg='다날',
method='본인인증',
order_name='회원가입 본인인증',
authenticate_type='sms',
username='홍길동',
identity_no='9001011', # 생년월일 6자리 + 주민등록번호 뒤 첫 1자리 = 7자리
carrier='SKT',
phone='01012345678',
client_ip=request.headers.get('X-Forwarded-For', request.remote_addr).split(',')[0]
)
# receipt_id를 세션·DB에 보관했다가 승인 단계에서 사용한다.
print(requested.get('receipt_id'))pythonBootpayApi::setClientKeyConfiguration('[ Client Key ]', '[ Secret Key ]');
// 사용자 휴대폰으로 인증번호가 전송된다.
$requested = BootpayApi::requestAuthentication([
'authentication_id' => 'auth_' . time(),
'pg' => '다날',
'method' => '본인인증',
'order_name' => '회원가입 본인인증',
'authenticate_type' => 'sms',
'username' => '홍길동',
'identity_no' => '9001011', // 생년월일 6자리 + 주민등록번호 뒤 첫 1자리 = 7자리
'carrier' => 'SKT',
'phone' => '01012345678',
'client_ip' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']
]);
// receipt_id를 세션·DB에 보관했다가 승인 단계에서 사용한다.
echo $requested->receipt_id;phpimport kr.co.bootpay.pg.Bootpay;
import kr.co.bootpay.pg.model.request.Authentication;
Bootpay bootpay = Bootpay.withClientKey("[ Client Key ]", "[ Secret Key ]");
Authentication auth = new Authentication();
auth.authenticationId = "auth_" + System.currentTimeMillis();
auth.pg = "다날";
auth.method = "본인인증";
auth.orderName = "회원가입 본인인증";
auth.authenticateType = "sms";
auth.username = "홍길동";
auth.identityNo = "9001011"; // 생년월일 6자리 + 주민등록번호 뒤 첫 1자리 = 7자리
auth.carrier = "SKT";
auth.phone = "01012345678";
auth.clientIp = clientIp;
// 사용자 휴대폰으로 인증번호가 전송된다.
HashMap<String, Object> requested = bootpay.requestAuthentication(auth);
// receipt_id를 세션·DB에 보관했다가 승인 단계에서 사용한다.
String receiptId = (String) requested.get("receipt_id");javabootpay = Bootpay::Api.new(client_key: '[ Client Key ]', secret_key: '[ Secret Key ]')
# 사용자 휴대폰으로 인증번호가 전송된다.
requested = bootpay.request_authentication(
authentication_id: "auth_#{Time.now.to_i}",
pg: '다날',
method: '본인인증',
order_name: '회원가입 본인인증',
authenticate_type: 'sms',
username: '홍길동',
identity_no: '9001011', # 생년월일 6자리 + 주민등록번호 뒤 첫 1자리 = 7자리
carrier: 'SKT',
phone: '01012345678',
client_ip: request.env['HTTP_X_FORWARDED_FOR'] || request.ip
)
# receipt_id를 세션·DB에 보관했다가 승인 단계에서 사용한다.
receipt_id = requested.data['receipt_id']rubyapi := bootpay.NewAPIWithClientKey("[ Client Key ]", "[ Secret Key ]", nil, "")
authData := bootpay.Authentication{
AuthenticationId: fmt.Sprintf("auth_%d", time.Now().UnixMilli()),
Pg: "다날",
Method: "본인인증",
OrderName: "회원가입 본인인증",
AuthenticateType: "sms",
Username: "홍길동",
IdentityNo: "9001011", // 생년월일 6자리 + 주민등록번호 뒤 첫 1자리 = 7자리
Carrier: "SKT",
Phone: "01012345678",
ClientIp: clientIp,
}
// 사용자 휴대폰으로 인증번호가 전송된다.
requested, err := api.RequestAuthentication(authData)
if err != nil {
return err
}
// receipt_id를 세션·DB에 보관했다가 승인 단계에서 사용한다.
receiptId := requested["receipt_id"].(string)gousing Bootpay;
using Bootpay.models;
var api = BootpayApi.WithClientKey("[ Client Key ]", "[ Secret Key ]");
var auth = new Authentication
{
authenticationId = $"auth_{DateTimeOffset.Now.ToUnixTimeMilliseconds()}",
pg = "다날",
method = "본인인증",
orderName = "회원가입 본인인증",
authenticateType = "sms",
username = "홍길동",
identityNo = "9001011", // 생년월일 6자리 + 주민등록번호 뒤 첫 1자리 = 7자리
carrier = "SKT",
phone = "01012345678",
clientIp = clientIp
};
// 사용자 휴대폰으로 인증번호가 전송된다.
var response = await api.RequestAuthentication(auth);
var requested = JsonConvert.DeserializeObject<Dictionary<string, object>>(
await response.Content.ReadAsStringAsync()
);
// receipt_id를 세션·DB에 보관했다가 승인 단계에서 사용한다.
var receiptId = requested["receipt_id"].ToString();csharp② 인증 승인
사용자가 화면에 입력한 인증번호(OTP)를 서버로 받아 승인해요. 응답의 status가 12면 인증 완료예요.
https://api.bootpay.co.kr/v2/authenticate/confirmBasic Auth| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
receipt_id |
String | 필수 | ① 인증 요청 응답으로 받은 값 |
otp |
String | 선택 | 사용자가 입력한 인증번호. sms 방식에서는 필수예요 |
const confirmed = await Bootpay.confirmAuthentication(receipt_id, otp)
// status 12(본인인증완료)일 때만 인증 완료로 처리한다.
if (confirmed.status !== 12) {
throw new Error('본인인증이 완료되지 않았습니다.')
}
const data = confirmed.authenticate_data
await saveVerifiedUser({
name: data.name,
birth: data.birth,
ci: data.unique, // unique = CI(연계정보). 기관 공통 식별값
di: data.di // di = DI(중복가입 확인정보). 가맹점별 식별값
})javascriptconfirmed = bootpay.confirm_authentication(receipt_id=receipt_id, otp=otp)
# status 12(본인인증완료)일 때만 인증 완료로 처리한다.
if confirmed.get('status') != 12:
raise Exception('본인인증이 완료되지 않았습니다.')
# unique = CI(연계정보, 기관 공통), di = DI(중복가입 확인정보, 가맹점별)
data = confirmed.get('authenticate_data')
save_verified_user(
name=data.get('name'),
birth=data.get('birth'),
ci=data.get('unique'),
di=data.get('di')
)python$confirmed = BootpayApi::confirmAuthentication($receiptId, $otp);
// status 12(본인인증완료)일 때만 인증 완료로 처리한다.
if ($confirmed->status !== 12) {
throw new Exception('본인인증이 완료되지 않았습니다.');
}
// unique = CI(연계정보, 기관 공통), di = DI(중복가입 확인정보, 가맹점별)
$data = $confirmed->authenticate_data;
saveVerifiedUser($data->name, $data->birth, $data->unique, $data->di);phpHashMap<String, Object> confirmed = bootpay.confirmAuthentication(receiptId, otp);
// status 12(본인인증완료)일 때만 인증 완료로 처리한다.
if (((Number) confirmed.get("status")).intValue() != 12) {
throw new IllegalStateException("본인인증이 완료되지 않았습니다.");
}
// unique = CI(연계정보, 기관 공통), di = DI(중복가입 확인정보, 가맹점별)
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) confirmed.get("authenticate_data");
saveVerifiedUser(data.get("name"), data.get("birth"), data.get("unique"), data.get("di"));javaconfirmed = bootpay.confirm_authentication(receipt_id, otp: otp).data
# status 12(본인인증완료)일 때만 인증 완료로 처리한다.
raise '본인인증이 완료되지 않았습니다.' if confirmed['status'] != 12
# unique = CI(연계정보, 기관 공통), di = DI(중복가입 확인정보, 가맹점별)
data = confirmed['authenticate_data']
save_verified_user(name: data['name'], birth: data['birth'], ci: data['unique'], di: data['di'])rubyconfirmed, err := api.ConfirmAuthentication(bootpay.AuthenticationParams{
ReceiptId: receiptId,
Otp: otp,
})
if err != nil {
return err
}
// status 12(본인인증완료)일 때만 인증 완료로 처리한다.
if int(confirmed["status"].(float64)) != 12 {
return errors.New("본인인증이 완료되지 않았습니다.")
}
// unique = CI(연계정보, 기관 공통), di = DI(중복가입 확인정보, 가맹점별)
data := confirmed["authenticate_data"].(map[string]interface{})
saveVerifiedUser(data["name"], data["birth"], data["unique"], data["di"])govar confirmResponse = await api.ConfirmAuthentication(new AuthenticationParams
{
receiptId = receiptId,
otp = otp
});
var confirmed = JsonConvert.DeserializeObject<Dictionary<string, object>>(
await confirmResponse.Content.ReadAsStringAsync()
);
// status 12(본인인증완료)일 때만 인증 완료로 처리한다.
if (Convert.ToInt32(confirmed["status"]) != 12)
{
throw new InvalidOperationException("본인인증이 완료되지 않았습니다.");
}
// unique = CI(연계정보, 기관 공통), di = DI(중복가입 확인정보, 가맹점별)
var data = (JObject)confirmed["authenticate_data"];
SaveVerifiedUser(data["name"], data["birth"], data["unique"], data["di"]);csharp승인 응답의 구조는 인증 정보 조회와 같아요. 응답 필드 전체는 인증 정보 조회·검증에서 확인해요.
③ 인증번호 재전송
사용자가 인증번호를 받지 못했을 때 재전송해요. 재전송 횟수는 재전송 API 응답의 authenticate_data.number_of_realarms로 확인할 수 있어요. 인증 정보 조회 응답에는 이 값이 들어 있지 않아요.
https://api.bootpay.co.kr/v2/authenticate/realarmBasic Auth| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
receipt_id |
String | 필수 | ① 인증 요청 응답으로 받은 값 |
await Bootpay.realarmAuthentication(receipt_id)javascriptbootpay.realarm_authentication(receipt_id=receipt_id)pythonBootpayApi::realarmAuthentication($receiptId);phpbootpay.realarmAuthentication(receiptId);javabootpay.realarm_authentication(receipt_id)ruby_, err := api.RealarmAuthentication(bootpay.AuthenticationParams{ReceiptId: receiptId})goawait api.RealarmAuthentication(new AuthenticationParams { receiptId = receiptId });csharp"인증번호 재전송" 버튼에는 쿨다운(예: 30초)을 두세요. 재전송이 무제한으로 호출되면 PG 측에서 차단될 수 있어요.
PASS 앱 인증 (authenticate_type: pass)
authenticate_type을 pass로 보내면 SMS 대신 PASS 앱으로 인증을 요청해요. 사용자는 PASS 앱에서 인증을 완료하고, 서버는 승인 API로 결과를 확인해요.
지원 여부는 인증 PG·계약 조건에 따라 다르므로, 사용 전에 관리자 설정과 계약 스펙을 확인해요.
에러 코드
| 코드 | 상황 | 처리 |
|---|---|---|
AUTH_NEED_PG_METHOD (2400) |
pg·method 누락 |
pg, method: '본인인증'을 채워요 |
AUTH_REQUEST_FAILED (2401) |
인증 요청이 실패했어요 | 파라미터(특히 identity_no, carrier)를 확인 후 재시도해요 |
AUTH_NOT_READY (2404) |
승인 대기(status: 51) 상태가 아니에요. 이미 승인 완료(12)된 건을 다시 승인·재전송해도 같은 코드예요 |
① 인증 요청부터 다시 진행해요 |
AUTH_CONFIRM_FAILED (2405) |
인증 승인이 실패했어요 | 인증번호가 맞는지 확인하고 재시도해요 |
AUTH_NOT_CONFIRMED (2408) |
아직 승인되지 않은 인증 건이에요 | 사용자 인증 완료 후 다시 조회해요 |
AUTH_EXPIRED (2409) |
인증 확인 유효시간(30분)이 지났어요 | 인증을 처음부터 다시 요청해요 |
AUTH_NOT_FOUND (2407) |
인증 정보를 찾지 못했어요 | receipt_id를 확인해요 |
다음 단계
- 인증 정보를 조회·저장하려면 → 인증 정보 조회·검증
- 인증창 방식으로 바꾸려면 → 인증창 연동
- 에러 코드 전체를 보려면 → 에러 코드표