가맹점이 구현하고 omni API 서버가 호출하는 표준 Bio API 스펙입니다.
이 API는 가맹점별 개별 adapter를 기본으로 하지 않고, omni가 제공하는 표준 계약을 가맹점이 구현하는 방식입니다.
Base URL#
가맹점별로 omni merchant 설정에 등록합니다.
https://merchant.example.com/omni/bio/v1
Authentication#
omni API 서버는 가맹점 바이오 API 호출 시 Ed25519 request signature를 사용합니다.
omni가 전역 서명 개인키로 요청에 서명하고, 가맹점은 omni가 공개한 공개키로 서명을 검증해서 요청이 진짜 omni API 서버에서 온 것인지 확인합니다. 가맹점은 OAuth token issuer를 구현하지 않으며, 검증용 비밀(secret)을 발급받거나 보관할 필요도 없습니다 — 공개키는 비밀이 아닙니다.
Headers#
X-Omni-Merchant-Id: merchant_123
X-Omni-Key-Id: omni-sign-2026-07
X-Omni-Timestamp: 2026-07-02T13:00:00Z
X-Omni-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-Omni-Content-SHA256: hex_sha256_raw_body
X-Omni-Signature: v2=base64_ed25519_signature
X-Request-Id: req_...
Idempotency-Key: enr_01J... # write request only
Content-Type: application/json
X-Omni-Key-Id는 omni 전역 서명키 id입니다 (가맹점별 값이 아님). 공개키 목록에서 이 id로 검증 키를 선택합니다.- signature header의 version prefix는
v2=입니다. 다른 prefix는 거부합니다.
Canonical Request#
서명 대상 문자열은 다음 순서와 newline을 고정합니다.
OMNI-ED25519
{http_method}
{path_and_query}
{timestamp}
{nonce}
{content_sha256}
Example:
OMNI-ED25519
POST
/omni/bio/v1/enrollments
2026-07-02T13:00:00Z
550e8400-e29b-41d4-a716-446655440000
8d969eef6ecad3c29a3a629280e686cff8fab8d8a39d7d...
Public Key Distribution#
omni 서명 공개키는 무인증 공개 엔드포인트로 게시합니다.
GET {omni_api_base}/v1/signing-keys
{
"keys": [
{
"key_id": "omni-sign-2026-07",
"algorithm": "ed25519",
"public_key_base64": "MCowBQYDK2Vw...",
"status": "active"
}
]
}
public_key_base64는 SPKI DER를 base64 인코딩한 값입니다 (대부분의 언어 표준 라이브러리가 바로 읽는 형식).status는active(현재 서명에 사용) 또는retiring(rotation 중 병행 게시되는 구 키)입니다.- 가맹점은 요청의
X-Omni-Key-Id로 키를 선택합니다. key_id 기준으로 캐시하고, 모르는 key_id가 오면 목록을 재조회한 뒤 그래도 없으면 거부합니다. - 공개키는 연동 시 안전 채널로 전달받을 필요가 없습니다 — 이 엔드포인트와 연동 문서가 배포 채널입니다.
Key Rotation#
서명키 교체는 공개키 병행 게시로 무중단 진행합니다.
- omni가 새 키를
active, 기존 키를retiring으로/v1/signing-keys에 병행 게시하고 새 키로 서명을 시작합니다. - 가맹점은 key_id 기준 캐시 덕분에 추가 작업이 없습니다 (모르는 key_id 수신 시 목록 재조회만 구현되어 있으면 됨).
- 유예 기간(가맹점 공지 후 최소 7일) 뒤
retiring키를 목록에서 제거합니다.
Verification Rules#
가맹점 바이오 API는 다음 순서로 검증합니다.
- 필수 header 존재 여부를 확인합니다.
X-Omni-Key-Id로 omni 서명 공개키를 찾습니다 (/v1/signing-keys캐시).X-Omni-Merchant-Id가 가맹점 자신에게 발급된merchant_id와 일치하는지 확인합니다.- timestamp 허용 오차를 확인합니다. MVP 기본값은 5분입니다.
- nonce가 재사용되지 않았는지 확인합니다.
- raw request body bytes로
X-Omni-Content-SHA256을 다시 계산합니다. - canonical request를 구성합니다.
X-Omni-Signature의v2=뒤 base64 값을 Ed25519 서명 검증합니다.- 검증 성공 후에만 저장/조회 요청을 처리합니다.
Security Notes#
features,enc_key는 가맹점 DB에 저장되는 바이오 등록 데이터입니다.- 가맹점은 Bio API request/response body를 access log, application log, APM, tracing, error report에 남기기 금지
- signature mismatch 로그에도 request body 원문 남기기 금지
- nonce 저장소 장애 시 fail open 금지
- 서명 검증에 필요한 것은 공개키뿐이므로 가맹점 측에 유출 시 서명 위조로 이어지는 비밀이 없습니다. omni 서명 개인키는 omni만 보관합니다.
Common Error Response#
{
"error": {
"code": "signature_mismatch",
"message": "Request signature is invalid.",
"request_id": "req_..."
}
}
Error Codes#
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_request | 필수 field 누락 또는 형식 오류 |
| 401 | missing_signature | 서명 header 누락 |
| 401 | invalid_signature | signature 형식 오류 또는 알 수 없는 key_id |
| 401 | signature_mismatch | signature 검증 실패 |
| 401 | timestamp_out_of_range | timestamp 허용 오차 초과 |
| 409 | nonce_reused | nonce 재사용 |
| 409 | idempotency_conflict | 같은 idempotency key로 다른 body 요청 |
| 404 | bio_enrollment_not_found | 기존 등록 데이터 없음 |
| 400 | unsupported_finger | 지원하지 않는 finger code |
| 400 | unsupported_bio_type | 지원하지 않는 bio type |
| 500 | merchant_bio_internal_error | 가맹점 바이오 API 내부 오류 |
POST/enrollments#
등록 Flow에서 바이오 인증앱 callback으로 받은 바이오 등록 데이터를 가맹점 DB에 저장합니다.
재등록(overwrite) 정책: 같은 (merchant_user_ref + finger)로 다시 저장하면 기존 등록 데이터를 덮어씁니다(최신 등록이 유효). 재등록은 정상 시나리오(기기 변경, 품질 개선 등)입니다. 가맹점이 원하면 재등록 시 자체 추가 인증을 요구하는 것은 가맹점 재량입니다.
Method#
POST
Request#
{
"merchant_id": "merchant_123",
"merchant_user_ref": "user_abc",
"provider_id": "pointlink",
"enrollment_session_id": "enr_01J...",
"type": "fingerprint",
"finger": "LI",
"features": "base64-or-provider-encoded-features",
"enc_key": "base64-or-provider-encoded-key",
"captured_at": "2026-07-02T13:03:00Z"
}
| Field | Type | Required | Description |
|---|---|---|---|
merchant_id | string | yes | 가맹점 ID |
merchant_user_ref | string | yes | 가맹점 로그인 사용자 참조값. omni에 전달되는 값만으로 개인을 특정할 수 없도록, 개인정보 원문이 아닌 가맹점 내부 불투명 참조값을 사용. 예: 전화번호 원문 대신 SHA-256(정규화된 전화번호) 값을 전달 |
provider_id | string | yes | MVP는 pointlink |
enrollment_session_id | string | yes | omni 등록 세션 ID |
type | string | yes | MVP는 fingerprint |
finger | string | yes | PointLink finger code |
features | string | yes | 바이오 인증앱이 생성한 바이오 등록 데이터 |
enc_key | string | yes | 바이오 인증앱이 생성한 암호화 key |
captured_at | string | no | 앱 캡처 시각. 없으면 서버 수신 시각 사용 가능 |
Response 200#
{
"stored": true,
"merchant_user_ref": "user_abc",
"provider_id": "pointlink",
"type": "fingerprint",
"finger": "LI",
"enrollment_ref": "bio_enr_789",
"stored_at": "2026-07-02T13:03:02Z"
}
Idempotency#
omni는 Idempotency-Key header에 enrollment_session_id를 보냅니다.
가맹점 바이오 API는 같은 Idempotency-Key와 같은 request body가 다시 들어오면 기존 저장 결과를 반환합니다. 같은 key로 다른 body가 들어오면 409 idempotency_conflict를 반환합니다.
Timeout and Retry#
omni client 기준:
request timeout: 5s (연결 수립 포함, 요청 전체 기준)
retry: network error, timeout, 5xx에 한해 1회
4xx: retry 없음
omni는 retry를 위해 features, enc_key를 DB나 durable queue에 저장하지 않습니다. retry는 현재 request memory 안에서만 수행합니다.
POST/enrollments/lookup#
확인 Flow에서 기존 등록 데이터를 조회합니다.
Method#
POST
조회 요청도 body를 사용합니다. merchant_user_ref를 URL path나 query에 넣지 않아 access log 노출 가능성을 줄입니다. 이 값 자체도 이메일, 전화번호, 이름, 주민등록번호 등 개인정보 원문이 아니어야 합니다.
Request#
{
"merchant_id": "merchant_123",
"merchant_user_ref": "user_abc",
"provider_id": "pointlink",
"verification_session_id": "ver_01J...",
"type": "fingerprint",
"finger": "LI"
}
| Field | Type | Required | Description |
|---|---|---|---|
merchant_id | string | yes | 가맹점 ID |
merchant_user_ref | string | yes | 가맹점 로그인 사용자 참조값. omni에 전달되는 값만으로 개인을 특정할 수 없도록, 개인정보 원문이 아닌 가맹점 내부 불투명 참조값을 사용. 예: 전화번호 원문 대신 SHA-256(정규화된 전화번호) 값을 전달 |
provider_id | string | yes | MVP는 pointlink |
verification_session_id | string | yes | omni 확인 세션 ID |
type | string | yes | MVP는 fingerprint |
finger | string | yes | 조회할 finger code |
Response 200#
{
"found": true,
"merchant_user_ref": "user_abc",
"provider_id": "pointlink",
"type": "fingerprint",
"finger": "LI",
"features": "base64-or-provider-encoded-features",
"enc_key": "base64-or-provider-encoded-key",
"enrollment_ref": "bio_enr_789",
"enrolled_at": "2026-07-02T12:30:00Z"
}
Response 404#
{
"error": {
"code": "bio_enrollment_not_found",
"message": "Bio enrollment was not found.",
"request_id": "req_..."
}
}
Security Notes#
- 응답의
features,enc_key는 omni API 서버가 확인 처리 중에만 사용합니다. - omni는 조회 응답의 바이오 payload를 DB, durable queue, 로그에 저장하지 않습니다.
- 가맹점은 필요한 사용자와 finger에 해당하는 최소 등록 데이터만 반환합니다.