개발자 문서 — 센터 Open API
개요
- Base 경로
/open-api/v1· 형식 JSON(UTF-8) · 시각은 UTC ISO-8601 입니다. 서명하는 경로는 /open-api/v1/... 전체입니다. - 기관은 분할한 얼굴 템플릿의 센터 보관 조각(receipt)을 저장·조회·암호 파기하고, 호환 인증을 통과한 벤더 목록을 조회합니다.
- 운영 구성에서는 Open API 가 연계 전용 mTLS 포트(기본
9143)에서만 열립니다. 개발용 plain 구성에서는 포털과 같은 포트(9150)입니다. - 고객 식별자는 평문이 아니라 기관이 만든 가명(HMAC-SHA256, 64자리 소문자 hex)으로만 보냅니다. 센터는 receipt 를 실제 고객과 연결할 수 없습니다.
인증(3중)
- mTLS — 클라이언트 인증서 필수(TLS 1.3). 인증서 CN 은 기관에 등록된 값과 같아야 합니다.
- 요청 서명 — 헤더 4개를 보냅니다.
헤더 값 X-Inst-Code기관 코드(예: NH001) X-Timestampepoch 초(밀리초·소수 불가). 센터 시각과 ±60초(경계 포함) 이내 X-Nonce요청마다 새로 만드는 값 [A-Za-z0-9_.:-]{8,80}. 기관별로 약 5분 동안 기억하며 한 번만 사용 가능X-Signature아래 규칙의 HMAC-SHA256, 소문자 hex canonical = METHOD + "\n" + 경로?쿼리 + "\n" + X-Timestamp + "\n" + X-Nonce + "\n" + SHA-256(본문 바이트, 소문자 hex) X-Signature = HMAC-SHA256(인증키, canonical) 의 소문자 hex본문이 없으면 빈 바이트열의 SHA-256(
e3b0c442…b855)을 씁니다. 경로·쿼리는 실제로 보내는 문자열 그대로 서명해야 합니다(쿼리 순서·인코딩 포함). - 재전송 방지 — 시각 오차 ±60초(경계 포함, 그래서 서명은 최대 약 2분 통과 가능), nonce 는 약 5분 동안 기억하며 재사용 거부(
REPLAY_DETECTED).
허용 IP 가 등록된 기관은 허용 대역 밖 호출이 거부됩니다. 허용 IP 는 센터가 보는 접속 주소 기준입니다: 로드밸런서(LB)가 HTTP 모드로 요청을 전달하면 센터는 신뢰하도록 설정된 프록시가 붙인 X-Forwarded-For 의 원 클라이언트 주소로 판정하지만, mTLS 를 L4(TCP) 로 그대로 통과(passthrough)시키는 구성에서는 센터가 원 IP 를 볼 수 없으므로 IP 허용 목록은 LB·방화벽 ACL 에서 강제해야 합니다(센터에서는 LB 주소로만 보임). 인증 실패의 사유(키·시각·허용 IP·정지·인증서 CN)는 보안상 구분해서 알려 주지 않으며 모두 AUTH_FAILED 입니다.
인증키 교체: 운영팀이 인증키를 재발급하면 이전 키가 유예 시간(기본 10분) 동안 함께 허용됩니다. 그 안에 새 키로 바꾸세요.
서명 생성기
서명 대상 문자열(canonical)
요청 헤더
- X-Inst-Code
- X-Timestamp
- X-Nonce
- X-Signature
- 본문 SHA-256
curl 명령
거래 ID 와 응답 헤더
기관의 한 거래(등록·인증·파기)가 센터 호출을 여러 번 만들 수 있으므로, 거래마다 만든 ID(UUID 권장)를 X-Request-Id 요청 헤더로 보내세요.
센터는 이 값을 처리 단계 기록(수신 → 인증 → 처리 → 응답)의 상관키로 쓰고 응답 헤더로 그대로 돌려줍니다. 서명 대상에는 포함되지 않습니다. 형식은 [A-Za-z0-9._:-]{8,40}, 없거나 맞지 않으면 센터가 만듭니다.
| 응답 헤더 | 의미 |
|---|---|
X-Request-Id | 거래 ID 에코. 오류 본문의 requestId 와 같은 값입니다. |
X-Center-Node | 요청을 처리한 센터 노드(예: kftc-a) |
X-Process-Ms | 센터 내부 처리 시간(밀리초, 올림 정수) |
X-Center-Result | STORED(신규 저장, 확인 대기) · UNCHANGED(동일 내용 재전송) · CONFIRMED(확인 완료·멱등 재확인) · SERVED(조회 제공) · DESTROYED(암호 파기) · NOT_FOUND(receipt 없음·파기·타 기관) · REJECTED(인증 실패·재전송·한도·검증 실패 등 모든 거부) · OK(그 밖의 성공: 호환 벤더 조회·결과 보고·상태 확인) |
응답 본문(DTO)은 바뀌지 않았습니다. 인증 실패의 구체적 사유는 호출자에게 알려 주지 않으며(센터 운영자 화면에서만 확인), X-Center-Result 도 거부는 REJECTED 하나로만 내려갑니다.
등록은 2단계: 저장(PUT) → 확인(confirm)
기관이 등록 도중 센터 응답을 못 받아(타임아웃) 실패 처리해도 센터에 receipt 가 남아 소유 매핑 없이 보관되는 일(고아 receipt)을 막기 위해 2단계 저장을 씁니다.
기관 센터
│ PUT /receipts/{id} (receipt 본문) │
│ ─────────────────────────────────────────────▶ │ receipt 를 PENDING 으로 암호화 저장, 확인 기한 = 지금 + 300초
│ ◀──────────────────────────────── 201 STORED ─ │ (이때 GET 은 404 — 아직 조회·인증에 쓸 수 없음)
│ (기관 쪽 등록 마무리: 티켓·소유 매핑 저장) │
│ POST /receipts/{id}/confirm (본문 없음) │
│ ─────────────────────────────────────────────▶ │ PENDING → ACTIVE
│ ◀────────────────────────────── 200 CONFIRMED ─ │ 이제 GET 조회·인증 가능
│ │
│ (confirm 을 못 보냄/실패 → 기한 경과) │ 센터가 자동으로 암호 파기(키·본문 삭제) + 파기 원장 UNCONFIRMED_EXPIRED
- 기관은 자기 쪽 등록(티켓 저장·소유 매핑)을 마친 뒤에 confirm 을 호출하세요. 그러면 센터에는 기관이 아는 receipt 만 ACTIVE 로 남습니다.
PUT응답: 201(신규)·헤더X-Center-Result: STORED. 확인 대기 중 같은 receipt 본문(SHA-256 이 같음)을 다시 PUT 하면 확인 기한을 다시 300초로 늘리고 200(UNCHANGED), 다른 내용이면 409.confirm: 멱등(이미 ACTIVE 면 200). 응답 DTO 는 PUT 과 같고(receiptId, storedAt, receiptSha256) 헤더는X-Center-Result: CONFIRMED. 없음·확인 기한 만료·이미 파기됨·다른 기관 소유는 모두 404RECEIPT_NOT_FOUND(구분하지 않음) — 404 면 새 receiptId 로 PUT 부터 다시 하세요(만료 파기된 receiptId 는 재사용할 수 없음).- 확인 전 receipt 는
GET·소유 확인에서 없음(404)으로 취급되고 접근 이력에도 남지 않습니다.DELETE도 확인 대기 receipt 는 404 입니다(기한이 지나면 센터가 정리). - 확인 기한(
poc.receipt.confirm-timeout-seconds, 기본 300초)을 지난 PENDING 은 센터가 30초 주기로 정리하므로 기한은 정확히 300초가 아니라 "최대 300초 + 정리 주기" 입니다. confirm 은 기한이 지난 뒤에는 정리 전이라도 404 입니다. - 센터 운영 설정
poc.receipt.require-confirm=false는 구 클라이언트 호환 모드(PUT 즉시 ACTIVE)이며 이때도 confirm 은 200 으로 안전하게 호출됩니다.
엔드포인트
| 방식 | 경로 | 설명 |
|---|---|---|
| PUT | /open-api/v1/receipts/{receiptId} | receipt 저장(멱등) — 확인 대기(PENDING) 로 저장. 201 신규 / 200 동일 내용 재전송 |
| POST | /open-api/v1/receipts/{receiptId}/confirm | receipt 확인(confirm) — PENDING → ACTIVE(멱등). 본문 없음. 확인해야 조회·인증에 쓸 수 있음 |
| GET | /open-api/v1/receipts/{receiptId}?purpose=VERIFY | receipt 조회. 조회마다 접근 이력이 남음. 확인(confirm) 전(PENDING)은 404 |
| DELETE | /open-api/v1/receipts/{receiptId}?reason=WITHDRAW | receipt 암호 파기(멱등). reason 필수, 같은 증적번호 반환 |
| GET | /open-api/v1/compat/vendors | 호환 인증 벤더 목록(CERTIFIED만 사용 가능) |
| POST | /open-api/v1/compat/report | 기관이 수행한 호환 인증 결과(점수·통과 여부)만 보고 |
| GET | /open-api/v1/health | 서명 불필요. 상태 확인 |
PUT 요청·응답 예시
PUT /open-api/v1/receipts/6de611e1-6861-40d7-bc5f-e00d4cddd060
{
"institutionCode": "NH001",
"customerRef": "9f2c4be1d7a3085c61e2f4ab93d70c58b1a64e0f7d2c93a85b6e10d4f7a2c9e3",
"vendorCode": "C019", "techCode": 105,
"divisionType": 1, "divisionRate": 5,
"receiptB64": "AAECAwQFBgcICQoLDA0ODw==",
"receiptSha256": "ad3d9ff4f5f2c8b0a53d5e1f2c0f6e8e1b7e43d3d5e1b0d6c6a5e2c1f1c4f9a2",
"channel": "APP"
}
201 Created
{ "receiptId": "6de611e1-…-e00d4cddd060", "storedAt": "2026-10-02T09:03:50.120Z", "receiptSha256": "ad3d9ff4…" }GET·DELETE 응답 예시
GET /open-api/v1/receipts/6de611e1-…?purpose=VERIFY → 200
{ "receiptId": "6de611e1-…", "receiptB64": "AAECAwQF…", "vendorCode": "C019", "techCode": 105,
"divisionType": 1, "divisionRate": 5, "receiptSha256": "ad3d9ff4…" }
DELETE /open-api/v1/receipts/6de611e1-…?reason=WITHDRAW → 200
{ "receiptId": "6de611e1-…", "destroyedAt": "2026-10-02T09:07:12.480Z", "certificateNo": "KFTC-D-20261002-00000006" }
오류(공통 형식) → 404
{ "code": 5014, "key": "RECEIPT_NOT_FOUND", "message": "receipt not found", "requestId": "3f9a1c0e7b2d4a56b8c1" }재시도·멱등 규칙
- PUT·DELETE 는 멱등입니다. 타임아웃이나 5xx 뒤에는 같은 요청을 그대로 다시 보내도 안전합니다. 단, 재시도할 때마다 nonce 와 서명은 새로 만들어야 합니다.
- PUT 에 같은 receiptId·같은 receipt 본문(SHA-256 이 같음)을 다시 보내면 200(확인 대기 중이면 확인 기한도 갱신), 다른 본문이면 409 ALREADY_ENROLLED, 파기된 receiptId 는 재사용할 수 없습니다.
- confirm 도 멱등입니다(이미 ACTIVE 면 200). 타임아웃·5xx 면 같은 confirm 을 다시 보내세요. confirm 이 404 면 receipt 가 만료 파기되었을 수 있으니 새 receiptId 로 PUT 부터 다시 하세요. PUT 응답을 못 받아도 확인 기한(기본 300초) 안에 같은 receipt 본문으로 PUT 을 재전송해 응답을 받고 confirm 하거나, 그냥 두면 센터가 자동으로 파기합니다(고아 receipt 가 남지 않음).
- DELETE 를 이미 파기된 건에 다시 보내면 같은 증적번호로 200 입니다.
- 429·503 은 지수 백오프(예: 1s → 2s → 4s, 최대 3~5회)로 재시도합니다. 503
OVERLOADED는 센터가 과부하라 즉시 또는 짧은 대기(기본 최대 0.4초) 뒤에 거부한 것으로 응답의Retry-After(초) 뒤에 재시도하세요(요청은 처리되지 않았습니다). 503SERVICE_UNAVAILABLE(5022)는 센터의 내부 저장소(DB) 장애로 즉시 거부한 것으로 마찬가지로Retry-After뒤에 재시도합니다(요청은 처리되지 않았습니다). 호출 한도 초과 외에도 센터가 평소와 다른 조회 패턴(짧은 시간의 대량·반복 조회, 없는 receipt 연속 조회)을 감지하면 그 기관의 receipt 조회(GET)만 일정 시간 429 로 거부될 수 있고(저장·파기·상태 확인은 영향 없음, 사유는 알려 주지 않음), 정상적인 인증 흐름(고객 1명당 receipt 1건 조회)에서는 일어나지 않습니다. 센터가 계속 응답하지 않으면 인증을 거부하는 쪽(fail-closed)으로 처리하세요.Retry-After는 503OVERLOADED·SERVICE_UNAVAILABLE·기동 중 503 에만 있고 429 에는 없습니다(이상 접근 차단은 기본 300초 이어질 수 있고 남은 시간을 알 수 없으니 지수 백오프로 간격을 늘리세요). - 400·401·404·409·422 는 재시도해도 같은 결과입니다. 요청을 고치거나 업무 처리를 하세요.
- 센터 복구(백업 복원) 직후에는 백업 이후 등록분이 없을 수 있어
RECEIPT_NOT_FOUND가 나오면 고객 재등록이 필요할 수 있습니다. - 문의할 때는 응답의 요청 ID(
requestId또는X-Request-Id헤더)를 알려 주세요.
오류 코드
필터·컨트롤러가 만드는 오류는 {code, key, message, requestId} 형식입니다(PUT·POST 에 Content-Type: application/json 이 아닌 본문을 보내면 415 + BAD_REQUEST). 존재하지 않는 경로(404)·허용되지 않는 메서드(405)·Accept 불일치(406)는 프레임워크 기본 응답({timestamp, status, error, path} 또는 빈 본문·HTML)이며 X-Center-Result: OK 가 붙습니다. 아래 표는 코드에서 직접 생성한 값입니다.
Swagger UI 에서 호출해 보기
Swagger UI는 명세를 읽고 요청 형식을 확인하는 용도로 쓰면 되고, Try it out 으로 실제 호출도 해 볼 수 있습니다(개발용 plain 구성). 요청마다 서명이 달라지므로 다음 순서를 따르세요.
- 이 페이지의 서명 생성기에 호출할 방식·경로(쿼리 포함)·본문을 Swagger 에서 보낼 것과 똑같이 입력하고 서명 계산을 누릅니다.
- Swagger UI 의 Authorize 에
X-Inst-Code,X-Timestamp,X-Nonce,X-Signature네 값을 붙여 넣습니다. - 같은 경로·본문으로 Try it out → Execute 합니다.
- 서명은
X-Timestamp가 센터 시각 ±60초 안일 때만 유효하고(서명 후 최대 약 2분), nonce 는 한 번만 쓸 수 있습니다. 다시 호출하려면 타임스탬프·nonce·서명을 새로 만드세요.
운영 구성(secure 프로파일)에서는 Open API 가 mTLS 포트에서만 열려 브라우저의 Swagger UI 로는 호출할 수 없으므로 명세 열람용으로 쓰고, 실제 호출은 클라이언트 인증서를 가진 프로그램에서 하세요.
예제 코드
#!/usr/bin/env bash
# 사용법: ./call.sh GET "/open-api/v1/compat/vendors"
# ./call.sh PUT "/open-api/v1/receipts/6de611e1-6861-40d7-bc5f-e00d4cddd060" '{"institutionCode":"NH001", ...}'
set -euo pipefail
BASE="http://localhost:9150" # 운영: https://센터주소:9143 (mTLS)
INST="NH001"
KEY="<발급받은 인증키>"
METHOD="$1"; PATHQ="$2"; BODY="${3:-}"
TS="$(date +%s)"
NONCE="$(uuidgen | tr 'A-Z' 'a-z')"
BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')"
SIG="$(printf '%s\n%s\n%s\n%s\n%s' "$METHOD" "$PATHQ" "$TS" "$NONCE" "$BODY_HASH" \
| openssl dgst -sha256 -hmac "$KEY" -hex | sed 's/^.* //')"
curl -sS -X "$METHOD" "$BASE$PATHQ" \
-H "Content-Type: application/json" \
-H "X-Inst-Code: $INST" -H "X-Timestamp: $TS" -H "X-Nonce: $NONCE" -H "X-Signature: $SIG" \
${BODY:+--data "$BODY"}
# mTLS 포트에서는 --cert client.pem --key client.key --cacert ca.pem 을 추가하세요.import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.UUID;
public class CenterClient {
static final String BASE = "http://localhost:9150"; // 운영: https://센터주소:9143 (mTLS 용 SSLContext 필요)
static final String INST = "NH001";
static final String KEY = "<발급받은 인증키>";
static String sha256Hex(byte[] b) throws Exception {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(b));
}
static String sign(String method, String pathQuery, String ts, String nonce, byte[] body) throws Exception {
String canonical = method + "\n" + pathQuery + "\n" + ts + "\n" + nonce + "\n" + sha256Hex(body);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return HexFormat.of().formatHex(mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)));
}
static HttpResponse<String> call(String method, String pathQuery, String json) throws Exception {
byte[] body = json == null ? new byte[0] : json.getBytes(StandardCharsets.UTF_8);
String ts = String.valueOf(System.currentTimeMillis() / 1000);
String nonce = UUID.randomUUID().toString(); // 재시도할 때마다 새로 만든다
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + pathQuery))
.method(method, body.length == 0 ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofByteArray(body))
.header("Content-Type", "application/json")
.header("X-Inst-Code", INST).header("X-Timestamp", ts).header("X-Nonce", nonce)
.header("X-Signature", sign(method, pathQuery, ts, nonce, body));
return HttpClient.newHttpClient().send(b.build(), HttpResponse.BodyHandlers.ofString());
}
public static void main(String[] args) throws Exception {
HttpResponse<String> r = call("GET", "/open-api/v1/compat/vendors", null);
System.out.println(r.statusCode() + " " + r.body());
}
}변경 이력
| 버전 | 일자 | 내용 |
|---|---|---|
| 1.3.2 | 2026-10-09 | 코드·실서버 교차 점검 반영: ① PUT·POST 의 Content-Type 이 application/json 이 아니면 415(BAD_REQUEST)로 답합니다(예전 500 INTEGRITY_ERROR). ② channel 이 10자를 넘으면 400(예전 500). ③ POST /compat/report 의 institutionCode 가 인증된 기관과 다르거나 없으면 401 AUTH_FAILED(예전 200 — 문서에 "같아야 함"이라고 적혀 있었으나 검증하지 않았음). ④ 명세 정정: 오류 응답 표(409·429·415·500 추가, GET·confirm 의 400), 확인 전(PENDING) receipt 의 DELETE 는 404, X-Center-Result 의 CONFIRMED, 서명 시각 ±60초(경계 포함)·nonce 약 5분, 본문 1MiB 초과는 401, 404·405·406 은 프레임워크 기본 응답. |
| 1.3.1 | 2026-10-03 | 센터 내부 저장소(DB) 장애 구분: DB 연결 불가·서킷 열림은 503 SERVICE_UNAVAILABLE(5022) + Retry-After 로 즉시 거부(이전에는 일부가 OVERLOADED·CENTER_UNAVAILABLE 로 나가고 요청 하나가 약 10초 걸릴 수 있었음) · 연결 풀 고갈만 OVERLOADED(5021) · 요청·응답 본문(DTO)·다른 헤더는 불변, 오류 키 SERVICE_UNAVAILABLE 한 가지 추가 · 과부하 보호 기본 상한을 서블릿 스레드 수의 3/4 로(스레드가 먼저 바닥나 연결 거부가 되는 일 방지)하고, 상한에 닿아도 최대 0.4초 대기열을 거친 뒤에(대기열이 가득 차면 즉시) 거부 — 응답 형식·헤더·오류 키는 그대로 |
| 1.3.0 | 2026-10-03 | 2단계 저장: PUT /receipts/{id} 는 receipt 를 확인 대기(PENDING)로 저장하고, 신규 POST /receipts/{id}/confirm(본문 없음, 멱등, 헤더 X-Center-Result: CONFIRMED)으로 확인해야 ACTIVE 가 됨 · 확인 전 GET 은 404 · 확인 기한(기본 300초) 안에 확인하지 않으면 센터가 암호 파기 · 요청·응답 본문(DTO)은 불변 · 센터 설정 poc.receipt.require-confirm=false 로 구 클라이언트 호환 · 허용 IP 는 센터가 보는 접속 주소 기준(로드밸런서·mTLS 통과 구성 주의) |
| 1.2.1 | 2026-10-03 | 모든 인증 실패(AUTH_FAILED)의 본문·메시지·상태·헤더를 사유와 무관하게 동일하게(authentication failed) · 재전송(409)·한도 초과(429)의 메시지를 request rejected 로 일반화(코드는 유지) · 비정상 접근 탐지 임계값을 기관별 호출 한도에 비례하게 · 과부하 시 503 OVERLOADED(5021) + Retry-After 로 즉시 거부(재시도 지침은 재시도·멱등 규칙 참고) |
| 1.2.0 | 2026-10-02 | 거래 ID(X-Request-Id) 요청 헤더 지원 · 모든 응답에 X-Request-Id·X-Center-Node·X-Process-Ms·X-Center-Result 헤더 추가(본문 변경 없음) · 평소와 다른 조회 패턴 감지 시 해당 기관의 receipt 조회가 일정 시간 429 RATE_LIMITED 로 거부될 수 있음 |
| 1.1.0 | 2026-10-02 | Swagger UI·OpenAPI 명세 제공 · 기관별 허용 IP · 인증키 재발급 시 이전 키 유예 허용 · 호환 벤더 상태를 CERTIFIED/SUSPENDED 로 표기 · DELETE 의 reason 필수 명시 · 오류 응답에 요청 ID 일관 제공 |
| 1.0.0 | 2026-10-02 | 최초 공개 — receipt 저장·조회·파기, 호환 벤더 조회·결과 보고, 상태 확인. 3중 인증(mTLS·요청 서명·재전송 방지) |