Authly 개발자 문서
가맹점 콘솔
문서 목록

가맹점 바이오 API

가맹점에서 직접 구현최종 수정 2026년 9월 18일

가맹점에서 구현하고 Authly가 호출하는 API 명세입니다. 등록 데이터 저장·조회와 Ed25519 서명 검증을 정의합니다.

목차

가맹점이 구현하고 Authly API 서버가 호출하는 표준 Bio API 스펙입니다.

이 API는 가맹점별 개별 adapter를 기본으로 하지 않고, Authly가 제공하는 표준 계약을 가맹점이 구현하는 방식입니다.

Base URL#

가맹점별로 Authly merchant 설정에 등록합니다. Base URL은 HTTPS만 등록할 수 있으며 credential, query, fragment를 포함할 수 없습니다.

https://merchant.example.com/authly/bio/v1

Authentication#

Authly API 서버는 가맹점 바이오 API 호출 시 Ed25519 request signature를 사용합니다.

Authly가 전역 서명 개인키로 요청에 서명하고, 가맹점은 Authly가 공개한 공개키로 서명을 검증해서 요청이 진짜 Authly API 서버에서 온 것인지 확인합니다. 가맹점은 OAuth token issuer를 구현하지 않으며, 검증용 비밀(secret)을 발급받거나 보관할 필요도 없습니다 — 공개키는 비밀이 아닙니다.

Headers#

X-Authly-Merchant-Id: merchant_123
X-Authly-Key-Id: authly-sign-2026-07
X-Authly-Timestamp: 2026-07-02T13:00:00Z
X-Authly-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-Authly-Content-SHA256: hex_sha256_raw_body
X-Authly-Signature: v2=base64_ed25519_signature
X-Request-Id: req_...
Idempotency-Key: enr_01J...   # write request only
Content-Type: application/json
  • X-Authly-Key-IdAuthly 전역 서명키 id입니다 (가맹점별 값이 아님). 공개키 목록에서 이 id로 검증 키를 선택합니다.
  • signature header의 version prefix는 v2=입니다. 다른 prefix는 거부합니다.

Canonical Request#

서명 대상 문자열은 다음 순서와 newline을 고정합니다.

AUTHLY-ED25519
{http_method}
{path_and_query}
{timestamp}
{nonce}
{content_sha256}

Example:

AUTHLY-ED25519
POST
/authly/bio/v1/enrollments
2026-07-02T13:00:00Z
550e8400-e29b-41d4-a716-446655440000
8d969eef6ecad3c29a3a629280e686cff8fab8d8a39d7d...

Public Key Distribution#

Authly 서명 공개키는 무인증 공개 엔드포인트로 게시합니다.

GET {AUTHLY_API_BASE}/v1/signing-keys
{
  "keys": [
    {
      "key_id": "authly-sign-2026-07",
      "algorithm": "ed25519",
      "public_key_base64": "MCowBQYDK2Vw...",
      "status": "active"
    }
  ]
}
  • public_key_base64SPKI DER를 base64 인코딩한 값입니다 (대부분의 언어 표준 라이브러리가 바로 읽는 형식).
  • statusactive(현재 서명에 사용) 또는 retiring(rotation 중 병행 게시되는 구 키)입니다.
  • 가맹점은 요청의 X-Authly-Key-Id로 키를 선택합니다. key_id 기준으로 캐시하고, 모르는 key_id가 오면 목록을 재조회한 뒤 그래도 없으면 거부합니다.
  • 공개키는 비밀이 아니며, 위 엔드포인트의 응답을 기준으로 사용합니다.

Key Rotation#

서명키 교체는 공개키 병행 게시로 무중단 진행합니다.

  1. 정기 교체는 전환 시각을 최소 7일 전에 공지합니다. 공지에는 새 key_id, 전환 시각과 구 키 제거 시각을 KST와 UTC로 함께 명시합니다.
  2. 전환 시 Authly가 새 키를 active, 기존 키를 retiring으로 /v1/signing-keys에 병행 게시하고 새 개인키로 서명을 시작합니다.
  3. 가맹점은 key_id 기준 캐시 덕분에 추가 작업이 없습니다 (모르는 key_id 수신 시 목록 재조회만 구현되어 있으면 됨).
  4. 전환 후 7일 동안 기존 공개키를 retiring으로 병행 게시한 뒤 목록에서 제거합니다.

개인키 유출 또는 침해가 의심되는 긴급 교체는 예외입니다. 이 경우 사전 공지와 7일 병행 기간 없이 즉시 새 키로 전환하고, 유출이 의심되는 기존 공개키를 목록에서 제거한 뒤 가맹점에 긴급 공지합니다.

Verification Rules#

가맹점 바이오 API는 다음 순서로 검증합니다.

  1. 필수 header 존재 여부를 확인합니다.
  2. X-Authly-Key-Id로 Authly 서명 공개키를 찾습니다 (/v1/signing-keys 캐시).
  3. X-Authly-Merchant-Id가 가맹점 자신에게 발급된 merchant_id와 일치하는지 확인합니다.
  4. timestamp 허용 오차를 확인합니다. MVP 기본값은 5분입니다.
  5. nonce가 재사용되지 않았는지 확인합니다.
  6. raw request body bytes로 X-Authly-Content-SHA256을 다시 계산합니다.
  7. canonical request를 구성합니다.
  8. X-Authly-Signaturev2= 뒤 base64 값을 Ed25519 서명 검증합니다.
  9. 검증 성공 후에만 저장/조회 요청을 처리합니다.

Security Notes#

  • payload.features, payload.enc_key는 가맹점 DB에 저장되는 바이오 등록 데이터입니다.
  • 가맹점은 Bio API request/response body를 access log, application log, APM, tracing, error report에 남기기 금지
  • signature mismatch 로그에도 request body 원문 남기기 금지
  • nonce 저장소 장애 시 fail open 금지
  • 서명 검증에 필요한 것은 공개키뿐이므로 가맹점 측에 유출 시 서명 위조로 이어지는 비밀이 없습니다. Authly 서명 개인키는 Authly만 보관합니다.

Common Error Response#

{
  "error": {
    "code": "signature_mismatch",
    "message": "Request signature is invalid.",
    "request_id": "req_..."
  }
}

Error Codes#

HTTPCodeDescription
400invalid_request필수 field 누락 또는 형식 오류
401missing_signature서명 header 누락
401invalid_signaturesignature 형식 오류 또는 알 수 없는 key_id
401signature_mismatchsignature 검증 실패
401timestamp_out_of_rangetimestamp 허용 오차 초과
409nonce_reusednonce 재사용
409idempotency_conflict같은 idempotency key로 다른 body 요청
404bio_enrollment_not_found기존 등록 데이터 없음
400unsupported_finger지원하지 않는 finger code
400unsupported_bio_type지원하지 않는 bio type
500merchant_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",
  "enrollment_session_id": "enr_01J...",
  "type": "fingerprint",
  "payload": {
    "finger": "LI",
    "features": "base64-or-provider-encoded-features",
    "enc_key": "base64-or-provider-encoded-key"
  },
  "captured_at": "2026-07-02T13:03:00Z"
}
FieldTypeRequiredDescription
merchant_idstringyes가맹점 ID
merchant_user_refstringyes가맹점 로그인 사용자 참조값. 개인정보 원문이나 단순 해시가 아닌 가맹점 내부 불투명 참조값
enrollment_session_idstringyesAuthly 등록 세션 ID
typestringyesMVP는 fingerprint. 인증 방식(payload 구조)을 구분하는 값이며, 향후 다른 방식이 추가되면 새 type 값과 그에 맞는 payload 구조가 추가됩니다
payloadobjectyestype에 따라 구조가 달라지는 등록 데이터. type: fingerprint의 구조는 아래와 같습니다
payload.fingerstringyes지원하는 finger code: LI, LM, RI, RM 중 하나
payload.featuresstringyes바이오 인증앱이 생성한 바이오 등록 데이터
payload.enc_keystringyes바이오 인증앱이 생성한 암호화 key
captured_atstringyesAuthly가 생성한 캡처 시각. 별도 캡처 시각이 없으면 callback 처리 시각

Response 200#

{
  "stored": true,
  "merchant_user_ref": "user_abc",
  "type": "fingerprint",
  "payload": { "finger": "LI" },
  "enrollment_ref": "bio_enr_789",
  "stored_at": "2026-07-02T13:03:02Z"
}

응답에서 stored: true는 필수입니다. merchant_user_ref, type, payload, enrollment_ref, stored_at은 선택값입니다. 선택 필드를 반환하면 merchant_user_ref, type은 요청값과 일치해야 하고, payload.finger를 반환하면 요청값과 일치해야 하며 지원 code여야 합니다. enrollment_ref는 빈 문자열일 수 없고 stored_at은 ISO 8601 timestamp여야 합니다. 불일치하거나 형식이 잘못된 응답은 세션 실패로 처리됩니다. payload.finger를 생략하면 Authly는 요청의 손가락 값을 사용합니다.

Idempotency#

Authly는 Idempotency-Key header에 enrollment_session_id를 보냅니다.

가맹점 바이오 API는 같은 Idempotency-Key와 같은 request body가 다시 들어오면 기존 저장 결과를 반환합니다. 같은 key로 다른 body가 들어오면 409 idempotency_conflict를 반환합니다.

Timeout and Retry#

Authly client 기준:

request timeout 기본값: 5s (연결 수립 포함, 요청 전체 기준)
retry 기본값: network error, timeout, 5xx에 한해 1회
4xx: retry 없음

timeout과 재시도 횟수는 가맹점별 설정값이며, 온보딩 시 다르게 설정할 수 있습니다.

Authly는 retry를 위해 payload.features, payload.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",
  "verification_session_id": "ver_01J...",
  "type": "fingerprint",
  "payload": {
    "finger": "LI"
  }
}
FieldTypeRequiredDescription
merchant_idstringyes가맹점 ID
merchant_user_refstringyes가맹점 로그인 사용자 참조값. 개인정보 원문이나 단순 해시가 아닌 가맹점 내부 불투명 참조값
verification_session_idstringyesAuthly 확인 세션 ID
typestringyesMVP는 fingerprint. 인증 방식(payload 구조)을 구분하는 값이며, 향후 다른 방식이 추가되면 새 type 값과 그에 맞는 payload 구조가 추가됩니다
payloadobjectyestype에 따라 구조가 달라지는 조회 조건. type: fingerprint의 구조는 아래와 같습니다
payload.fingerstringyes조회할 finger code: LI, LM, RI, RM 중 하나

Response 200#

{
  "found": true,
  "merchant_user_ref": "user_abc",
  "type": "fingerprint",
  "payload": {
    "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"
}

성공 응답에서 found: true, payload.features, payload.enc_key는 필수입니다. 나머지 필드는 선택값입니다. payload.featurespayload.enc_key는 빈 문자열일 수 없습니다. 선택 필드를 반환하면 merchant_user_ref, type은 요청값과 일치해야 하고, payload.finger를 반환하면 요청값과 일치해야 하며 지원 code여야 합니다. enrollment_ref는 빈 문자열일 수 없고 enrolled_at은 ISO 8601 timestamp여야 합니다. 불일치하거나 형식이 잘못된 응답은 세션 실패로 처리됩니다.

Response 404#

{
  "error": {
    "code": "bio_enrollment_not_found",
    "message": "Bio enrollment was not found.",
    "request_id": "req_..."
  }
}

Security Notes#

  • 응답의 payload.features, payload.enc_key는 Authly API 서버가 확인 처리 중에만 사용합니다.
  • Authly는 조회 응답의 바이오 payload를 DB, durable queue, 로그에 저장하지 않습니다.
  • 가맹점은 필요한 사용자와 finger에 해당하는 최소 등록 데이터만 반환합니다.