빠른 시작
- 1
세션 만들기
백엔드에서 파트너 키로
POST /v1/partner/sessions를 호출합니다. - 2
흐름 실행
클라이언트 토큰을 SDK에 넘기거나, 브라우저에서
verificationUrl을 엽니다. - 3
결과 받기
웹훅 서명을 확인한 뒤 신원 ID와 상태를 저장합니다.
curl https://kyc.zoi.network/v1/partner/sessions \
-H "Authorization: Bearer $ZOI_ID_PARTNER_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: signup-user_42" \
-d '{ "externalUserId": "user_42", "level": "standard", "appId": "com.example.app" }'두 개의 API
파트너 API
서버 간 통신이며 /v1/partner 아래에 있습니다. 파트너 키를 Bearer 토큰으로 보냅니다. 앱에는 절대 넣지 마세요.
클라이언트 API
SDK가 /v1/client 아래에서 씁니다. 한 세션에 60분 동안 유효한 클라이언트 토큰을 보냅니다.
규칙
- 시간은 에포크 밀리초이며, 이름이
At으로 끝나는 필드에 들어갑니다. - 날짜는
YYYY-MM-DD형식입니다. 모르는 부분은XX로 씁니다. 예:1990-XX-XX. - 무언가를 만들거나 결정하는 POST 요청은
Idempotency-Key헤더를 받습니다. 키는 24시간 보관합니다. - 세션을 바꿀 때는 기대하는 리비전을 보냅니다. 리비전이 바뀌었으면
revision_conflict를 받습니다.
세션 흐름
백엔드가 세션을 만들고, SDK가 단계를 진행합니다. 서버는 순서를 확인하고, 너무 이른 단계에는 step_not_allowed로 답합니다.
1. 백엔드에서 세션 만들기
같은 사용자로 다시 호출하면 열린 세션을 이어 가고 새 클라이언트 토큰을 줍니다.
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json
Idempotency-Key: 6f1c2a9e-signup-42
{
"externalUserId": "user_42",
"level": "standard",
"locale": "en",
"appId": "com.example.app"
}
200 OK
{
"sessionId": "ses_...",
"clientToken": "...",
"clientTokenExpiresAt": 1767225600000,
"status": "created",
"apiBaseUrl": "https://kyc.zoi.network",
"resumed": false
}2. SDK가 진행하는 단계
- 01
세션과 아직 남은 단계를 읽습니다.
GET /v1/client/session
- 02
동의를 기록합니다. 얼굴 비교에 대한 별도의 선택 동의도 함께 기록합니다.
POST /v1/client/consent
- 03
앱과 기기를 확인합니다. 그 결과는 판정에 반영됩니다.
POST /v1/client/attestation/challenge, POST /v1/client/attestation
- 04
신분증을 보냅니다. MRZ 스캔 후 칩을 읽거나, 사진을 보냅니다.
POST /v1/client/document/mrz-scan, POST /v1/client/document/chip/challenge, POST /v1/client/document/chip or POST /v1/client/document/photos
- 05
앱이 고개 움직임을 보여 주는 동안 전면 카메라로 짧은 영상 하나를 녹화합니다.
POST /v1/client/liveness/challenge, POST /v1/client/liveness
- 06
거주 국가를 보내고, 강화 레벨에서는 주소도 보냅니다.
POST /v1/client/profile
- 07
주소 요건이 있을 때만: 서류를 올린 뒤 주소를 확인합니다. 신분증에서 이미 주소를 얻었다면 건너뜁니다.
POST /v1/client/address/document, POST /v1/client/address/confirm
- 08
자금 요건이 있을 때만: 답변을 저장하고, 정책이 요구하면 서류를 추가한 뒤 단계를 완료합니다.
POST /v1/client/funds/answers, POST /v1/client/funds/document, POST /v1/client/funds/remove, POST /v1/client/funds/complete
- 09
두 가지 질문에 답합니다. 정치적 주요 인물인지, 본인을 위해 행동하는지.
POST /v1/client/declarations
- 10
현재 리비전으로 제출합니다.
POST /v1/client/submit
사용자는 자동 결과를 사람이 다시 보도록 요청하거나(POST /review-request), 동의를 철회할 수도 있습니다(POST /withdraw). 철회하면 세션이 취소되고 파일이 바로 삭제됩니다.
주소 증명과 자금 출처
POST /v1/partner/sessions에 requirements를 추가하면 신원 외의 항목도 확인합니다. address는 주소 증명을 요청합니다. source_of_funds는 돈이 어디서 오는지 묻고, source_of_wealth는 총자산과 그 형성 과정까지 묻습니다. 두 자금 값은 합쳐서 하나의 단계가 되며, 둘 다 address와 함께 쓸 수 있습니다.
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json
{
"externalUserId": "user_42",
"level": "standard",
"appId": "com.example.app",
"requirements": ["address", "source_of_funds"],
// Optional. Without them your partner default applies, else ours.
"addressPolicy": { "maxAgeDays": 90, "signedOnly": false },
"fundsPolicy": { "evidence": "optional", "currency": "EUR" }
}선택 정책
정책이 없으면 파트너 기본값이, 그것도 없으면 저희 기본값이 적용됩니다. 정책은 세션을 만들 때 세션에 복사됩니다.
addressPolicy:maxAgeDays(기본 90, 1에서 366),acceptedTypes(기본: 공과금, 통신, 은행 서류, 세금 서류, 정부 서신, 거주 증명서),signedOnly(기본 false).fundsPolicy:questions,evidence(none,optional,required중 하나, 기본none),acceptedTypes,maxAgeDays(기본 180),currency(ISO 4217, 기본 USD), 범위를 정하는brackets.- 다른 요건으로 열려 있고 아직 제출되지 않은 세션은 레벨을 바꿀 때처럼 취소되고 새 세션이 시작됩니다.
address_signed_pdf,address_document_review,funds_signed_documents,funds_review같은 샌드박스 시나리오로 결과별로 테스트하세요.
후속 세션
이미 신원이 활성 상태인 사용자에 대해 요건만 확인합니다. requirements와 함께 reuseIdentity: true를 보내세요.
- 사용자는 동의한 뒤 새 단계만 마칩니다. 신분증, 셀피, 신고 사항을 다시 묻지 않습니다.
- 승인되면 신원은 ID, 확인 날짜, 유효 기간을 그대로 유지하고 새 결과를 받습니다.
session.approved와identity.updated를 받습니다. - 활성 신원이 없으면 호출은 409
step_not_allowed로 응답합니다. - 큰 금액의 입금, PEP 플래그, 위험 국가 같은 자체 리스크 규칙에 따라 시작하세요. 규칙은 백엔드 한곳에 그대로 둡니다.
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json
Idempotency-Key: edd-user_42-2026-11
{
"externalUserId": "user_42",
"appId": "com.example.app",
"requirements": ["source_of_funds", "source_of_wealth"],
"reuseIdentity": true,
"fundsPolicy": { "evidence": "required" }
}
// No active identity for user_42:
409 { "error": { "code": "step_not_allowed", ... } }결과와 스코프
승인된 세션에는 address와 funds가 담기며, 신원은 각각의 최신 결과를 유지합니다. 요약 항목은 모든 스코프에서 받고, 상세 정보에는 스코프가 필요합니다.
- 스코프
address: 주소. 이 스코프가 없으면verified,method,evidenceLevel,documentType,documentDate,verifiedAt,country를 받습니다. - 스코프
financial_data:answers,documents,findings. 이 스코프가 없으면kinds,evidenceLevel,reviewed,documentCount,verifiedAt를 받습니다.eligibility에는 절대 포함되지 않습니다. evidenceLevel은high,medium,low,none중 하나입니다. 주소 확인 방식은 강한 순서대로wallet,id_document,signed_pdf,official_code,document_review,declared입니다.- 서류는 API로 보내지 않습니다. 환경에
financial_data스코프가 있으면 컴플라이언스 역할을 가진 멤버가 대시보드에서 자금 출처 서류 페이지를 볼 수 있습니다. 모든 열람은 기록됩니다.
"session": {
"id": "ses_...",
"status": "approved",
"requirements": ["address", "source_of_funds"],
"reusesIdentity": false,
"address": {
"verified": true,
"method": "signed_pdf",
"evidenceLevel": "high",
"documentType": "bank_statement",
"documentDate": "2026-10-14",
"verifiedAt": 1767225600000,
"country": "DE",
// scope "address"
"address": { "line1": "Musterstrasse 12", "city": "Berlin", "postalCode": "10115", "country": "DE" }
},
"funds": {
"kinds": ["source_of_funds"],
"evidenceLevel": "medium",
"reviewed": true,
"documentCount": 2,
"verifiedAt": 1767225600000,
// scope "financial_data"
"answers": {
"employmentStatus": "employed",
"income": { "min": 50000, "max": 100000, "currency": "EUR" },
"sources": ["salary", "savings"],
...
},
"documents": [
{ "documentType": "payslip", "documentDate": "2026-10-31", "amount": 5200, "currency": "EUR",
"signature": "valid", "signedByKnownIssuer": true, "nameMatch": "match", "findings": [], ... },
...
],
"findings": []
}
}웹 흐름
SDK를 쓰지 않나요? 사용자를 저희가 호스팅하는 페이지로 보내세요. 같은 API, 같은 검사, 같은 검토 대기열을 사용합니다.
1. 링크 받기
POST /v1/partner/sessions는 verificationUrl을 반환합니다. 끝난 뒤 사용자를 돌려보내려면 redirectUrl을 넘기세요. 등록한 리디렉션 출처 중 하나로 시작해야 합니다.
POST /v1/partner/sessions
Authorization: Bearer <partner key>
Content-Type: application/json
{
"externalUserId": "user_42",
"level": "standard",
"locale": "en",
"appId": "com.example.app",
"redirectUrl": "https://example.com/kyc/done"
}
200 OK
{
"sessionId": "ses_...",
"clientToken": "...",
"verificationUrl": "https://kyc.zoi.network/verify/#token=...&lang=en",
...
}- 클라이언트 토큰은 URL의 프래그먼트, 즉
#뒤에 있습니다. 브라우저는 프래그먼트를 서버로 보내지 않으므로 토큰은 로그나Referer헤더에 남지 않습니다. 페이지는 토큰을 읽고 주소 표시줄에서 지운 뒤 메모리에만 보관합니다. - 19개 언어 중 하나를 고르려면 프래그먼트에
lang을 추가하세요. 없으면 브라우저 언어를 사용합니다. - 새로 고치면 토큰이 사라집니다. 백엔드에서 같은 세션의 새 링크를 만드세요.
- 페이지는 동의와 함께
channel: "web"을 보냅니다. 브라우저는 신분증 칩을 읽을 수 없으므로 웹 세션은 신분증 사진이나 디지털 신분증 지갑을 사용합니다. 기기 확인은 건너뛰며, 웹에서 온 신분증 사진은 항상 교육받은 검토자가 판정합니다. - 컴퓨터에서는 사용자가 휴대폰에서 계속할 수 있도록 QR 코드를 보여 줍니다.
2. 사용자가 끝냈을 때
흐름이 끝나면(제출, 취소 또는 최종 상태) 페이지는 다음 중 존재하는 모든 대상에 이 메시지를 보냅니다: window.ReactNativeWebView, window.webkit.messageHandlers.zoiId, window.ZoiIdAndroid, window.opener. 그런 다음 zoiIdSession과 zoiIdStatus를 붙여 redirectUrl을 엽니다.
메시지나 리디렉션 매개변수는 절대 믿지 마세요. Webhook이나 GET /v1/partner/sessions/:id만 믿으세요.
// Sent to each bridge that exists:
// window.ReactNativeWebView.postMessage(JSON string)
// window.webkit.messageHandlers.zoiId.postMessage(object)
// window.ZoiIdAndroid.postMessage(JSON string)
// window.opener.postMessage(object, "<origin of your redirectUrl>")
// (popups only, and only when the session has a redirectUrl)
{
"source": "zoi-id",
"type": "finished", // or "cancelled"
"sessionId": "ses_...",
"status": "processing" // the session status at that moment
}
// Then, if redirectUrl was set (the page uses the one stored with the
// session, never one from the address bar):
https://example.com/kyc/done?zoiIdSession=ses_...&zoiIdStatus=processing3. 웹뷰 설정
Android WebView
javaScriptEnabled와 domStorageEnabled를 켜고 mediaPlaybackRequiresUserGesture를 false로 설정하세요. 앱이 CAMERA 권한을 받은 뒤 WebChromeClient의 onPermissionRequest에서 RESOURCE_VIDEO_CAPTURE를 허용하세요. 사진 대체 경로를 위해 파일 선택기도 추가하세요.
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
// Only after your app holds Manifest.permission.CAMERA.
if (request.resources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
request.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
} else {
request.deny()
}
}
override fun onShowFileChooser(
view: WebView, callback: ValueCallback<Array<Uri>>, params: FileChooserParams,
): Boolean {
// Open a picker and pass the result to callback (photo fallback).
return true
}
}iOS WKWebView
iOS 14.3 이상. allowsInlineMediaPlayback을 true로 설정하고 앱에 NSCameraUsageDescription을 추가하세요. iOS 15 이상에서는 UI delegate에서 카메라 권한을 결정할 수 있습니다.
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
let webView = WKWebView(frame: .zero, configuration: config)
webView.uiDelegate = self // iOS 15+: decide camera access below
func webView(_ webView: WKWebView,
requestMediaCapturePermissionFor origin: WKSecurityOrigin,
initiatedByFrame frame: WKFrameInfo,
type: WKMediaCaptureType,
decisionHandler: @escaping (WKPermissionDecision) -> Void) {
decisionHandler(origin.host == "kyc.zoi.network" ? .grant : .deny)
}
// Info.plist: NSCameraUsageDescriptionReact Native WebView
mediaCapturePermissionGrantType="grant", allowsInlineMediaPlayback, javaScriptEnabled를 설정하세요. 앱에 카메라 권한이 필요합니다.
<WebView
source={{ uri: verificationUrl }}
javaScriptEnabled
allowsInlineMediaPlayback
mediaCapturePermissionGrantType="grant"
onMessage={(event) => {
const message = JSON.parse(event.nativeEvent.data);
if (message.source === "zoi-id") {
// Close the webview, then ask your backend for the real status.
}
}}
/>카메라 권한과 클릭재킹 때문에 iframe은 지원하지 않습니다. 브라우저 탭, 팝업 또는 웹뷰를 사용하세요.
상태
세션
created- 세션은 있지만 아직 아무것도 올리지 않았습니다.
in_progress- 동의했고, 단계를 올리는 중입니다.
processing- 제출했고, 확인이 진행 중입니다.
review- 교육받은 검토자를 기다리는 중입니다.
approved- 최종. 신원이 인증되었습니다.
rejected- 최종. 사유 코드를 확인하세요.
resubmit- 사용자가 requiredSteps의 단계를 다시 해야 합니다.
expired- 제때 최종 결정이 나지 않았습니다.
cancelled- 파트너, 사용자 또는 삭제로 취소되었습니다.
신원
active- 사용할 수 있습니다.
suspended- 보류 중입니다. 예를 들어 새 제재 후보가 나온 뒤.
expired- 신분증이나 인증 기간이 끝났습니다. 새 세션에서 다시 인증합니다.
revoked- 최종.
SDK
React Native (Expo) SDK는 타입이 있는 API 클라이언트, NFC 칩 읽기, 기기 확인을 제공합니다. 헤드리스 방식이라 화면과 카메라는 앱이 완전히 제어합니다. 서버에서는 TypeScript, Python, Go, PHP, Java, Ruby용 타입 지원 클라이언트를 사용하세요.
import { ZoiIdClient, nextStep } from "@zoi/id-sdk";
// clientToken and apiBaseUrl come from your backend (POST /v1/partner/sessions).
const client = new ZoiIdClient({ baseUrl: apiBaseUrl, clientToken });
const session = await client.getSession();
switch (nextStep(session)) {
case "consent": /* show the notice and the biometric choice */ break;
case "document": /* chip read or photos */ break;
case "liveness": /* record the video */ break;
case "address": /* upload a document, then confirm the address */ break;
case "funds": /* answers, then documents when the policy asks */ break;
case "submit": await client.submit(session.revision); break;
case "done": /* wait for your backend to get the webhook */ break;
}웹훅
세션이나 신원이 바뀌면 웹훅을 보냅니다. 내용을 믿기 전에 항상 서명을 확인하세요.
- 모든 웹훅에는
Zoi-Id-Signature헤더가 있습니다:t=unix seconds,v1=hex. v1은 텍스트t.raw body를 웹훅 시크릿으로 계산한 HMAC-SHA256입니다. JSON을 파싱하기 전의 원본 본문을 쓰세요.- 시크릿을 바꾸는 동안에는 활성 시크릿마다
v1이 하나씩 붙습니다. 그중 하나라도 맞으면 웹훅을 받아들이세요. t가 현재 시각과 300초 넘게 차이 나면 거부하세요.- 실패한 전송은 72시간 동안 다시 시도합니다. 다시 보낼 때마다 새
t로 다시 서명합니다. - 신원에는
revision이 있습니다. 저장한 것보다 새 리비전일 때만 이벤트를 반영하세요.
POST <your webhook URL>
Zoi-Id-Signature: t=1767225600,v1=5f2b...e9
{
"v": 1,
"eventId": "evt_...",
"type": "identity.suspended",
"createdAt": 1767225600000,
"object": "identity",
"externalUserId": "user_42",
"identity": {
"identityId": "idn_...",
"revision": 7,
"status": "suspended",
"level": "standard",
"verifiedAt": 1764547200000,
"validUntil": 1859155200000,
"ageOver18": true,
"captureMethod": "nfc"
}
}이벤트 종류
session.processingsession.reviewsession.approvedsession.rejectedsession.resubmitsession.expiredsession.cancelledidentity.updatedidentity.suspendedidentity.reinstatedidentity.expiredidentity.revoked
TypeScript로 서명 확인
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verifyZoiIdSignature(
rawBody: string,
header: string | null,
secret: string,
nowSeconds = Math.floor(Date.now() / 1000),
): boolean {
if (!header) return false;
let t: string | undefined;
const signatures: Buffer[] = [];
for (const part of header.split(",")) {
const [key, value] = part.trim().split("=");
if (!value) continue;
if (key === "t") t = value;
if (key === "v1" && /^[0-9a-f]+$/i.test(value)) signatures.push(Buffer.from(value, "hex"));
}
if (!t || !/^\d+$/.test(t) || signatures.length === 0) return false;
if (Math.abs(nowSeconds - Number(t)) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return signatures.some((sig) => sig.length === expected.length && timingSafeEqual(sig, expected));
}
// Example handler: read the raw body first, parse JSON only after the check.
export async function POST(request: Request) {
const rawBody = await request.text();
const ok = verifyZoiIdSignature(rawBody, request.headers.get("Zoi-Id-Signature"), process.env.ZOI_ID_WEBHOOK_SECRET!);
if (!ok) return new Response("bad signature", { status: 400 });
const event = JSON.parse(rawBody);
// Store event.eventId to skip duplicates, then apply it if the revision is newer.
return new Response(null, { status: 204 });
}Go로 서명 확인
package zoiid
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
)
const toleranceSeconds = 300
// VerifySignature checks the Zoi-Id-Signature header against the raw body.
func VerifySignature(rawBody []byte, header, secret string, now time.Time) bool {
var t string
var sigs [][]byte
for _, part := range strings.Split(header, ",") {
key, value, ok := strings.Cut(strings.TrimSpace(part), "=")
if !ok || value == "" {
continue
}
switch key {
case "t":
t = value
case "v1":
if b, err := hex.DecodeString(value); err == nil {
sigs = append(sigs, b)
}
}
}
ts, err := strconv.ParseInt(t, 10, 64)
if err != nil || len(sigs) == 0 {
return false
}
if d := now.Unix() - ts; d > toleranceSeconds || d < -toleranceSeconds {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(t + "."))
mac.Write(rawBody)
expected := mac.Sum(nil)
for _, sig := range sigs {
if hmac.Equal(sig, expected) {
return true
}
}
return false
}이벤트 피드
GET /v1/partner/events는 최근 30일의 모든 이벤트를 오래된 순서로 돌려줍니다. 다음 페이지는 마지막 nextCursor를 after에 넣어 읽습니다. 긴 장애 후나, 빠진 것이 없는지 확인할 때 쓰세요.
GET /v1/partner/events?after=<nextCursor>&limit=100
Authorization: Bearer <partner key>
200 OK
{
"items": [ /* webhook payloads, oldest first */ ],
"nextCursor": "..." // null when there is nothing more
}사기 신고
판정이 끝난 세션이 사기로 밝혀지면(예: 지불 거절 이후) 알려 주세요. 신고는 잘못된 승인을 측정하는 데 쓰입니다. 세션, 신원, 사용자의 결과는 바뀌지 않습니다.
- 스코프
sessions:write. 판정이 끝난 세션(승인 또는 거부)에만 해당합니다. 판정 전에는 409로 응답합니다. - 본문이 비어 있으면 유형
other의confirmed_fraud로 신고됩니다.status는suspected_fraud일 수도 있고,cleared로 직접 한 신고를 철회할 수도 있습니다. - 유형:
forged_document,impostor,deepfake_or_replay,synthetic_identity,account_takeover,other.cleared에는 유형을 붙이지 않습니다. note는 저희 분석가를 위한 자유 텍스트로, 최대 1000자입니다. 이름, 신분증 번호 같은 개인 데이터는 절대 넣지 마세요.- 새 신고에는 201로 응답합니다. 같은 신고를 다시 보내면 저장된 신고를 200으로 돌려줍니다. 안전하게 재시도하려면
Idempotency-Key를 보내세요. 세션당 최대 20건까지 신고할 수 있습니다. - 대시보드의 세션 페이지에서도 클릭 한 번으로 같은 신고를 할 수 있습니다.
POST /v1/partner/sessions/ses_.../fraud-reports
Authorization: Bearer <partner key>
Content-Type: application/json
Idempotency-Key: chargeback-8812
{
"status": "confirmed_fraud",
"category": "impostor",
"note": "Chargeback on the first deposit"
}
201 Created
{
"id": "flb_...",
"sessionId": "ses_...",
"status": "confirmed_fraud",
"category": "impostor",
"noteKept": true,
"createdAt": 1767225600000
}삭제
DELETE /v1/partner/users/:externalUserId는 사용자가 계정을 닫거나 삭제를 요청하면 그 데이터를 지웁니다. 응답에는 지금 지운 것과, 보관하는 것, 그 이유와 기한이 나옵니다.
DELETE /v1/partner/users/user_42
Authorization: Bearer <partner key>
200 OK
{
"deleted": ["images", "chip_files", "video", "personal_data", "address"],
"retained": [
{ "category": "dedup_keys", "reason": "fraud_prevention", "until": 1924992000000 },
{ "category": "sanctions_screening_records", "reason": "legal_obligation", "until": 2082758400000 }
],
"backupsPurgedWithinDays": 30
}오류
모든 오류는 같은 형태이며, 코드마다 고정된 HTTP 상태가 있습니다.
{
"error": {
"code": "step_not_allowed",
"message": "This step is not allowed now.",
"requestId": "req_..."
}
}| 코드 | HTTP | 의미 |
|---|---|---|
bad_request | 400 | 요청이 계약과 맞지 않습니다. |
unauthorized | 401 | 키나 토큰이 없거나 틀렸습니다. |
token_expired | 401 | 클라이언트 토큰이 만료되었습니다. 백엔드에서 새로 만드세요. |
forbidden | 403 | 이 키로는 할 수 없는 작업입니다. |
not_found | 404 | 그런 세션이나 사용자가 없습니다. |
session_closed | 409 | 세션이 확정되었습니다. 더 올릴 수 없습니다. |
step_not_allowed | 409 | 순서가 틀렸거나, 지금은 필요 없는 단계입니다. |
revision_conflict | 409 | 세션 리비전이 바뀌었습니다. 다시 읽으세요. |
idempotency_conflict | 409 | 같은 Idempotency-Key가 다른 요청에 쓰였습니다. |
challenge_expired | 410 | 챌린지가 만료되었거나 이미 쓰였습니다. 새로 요청하세요. |
payload_too_large | 413 | 업로드가 너무 큽니다. |
unsupported_media_type | 415 | 받지 않는 파일 형식입니다. |
rate_limited | 429 | 요청이 너무 많습니다. 나중에 다시 시도하세요. |
internal | 500 | 저희 쪽에서 문제가 생겼습니다. |
dependency_unavailable | 503 | 필요한 서비스가 멈췄습니다. 나중에 다시 시도하세요. |
사유 코드
다시 제출이 필요하거나 거절된 세션에는 reasonCodes가 있습니다. 코드마다 직접 번역한 메시지를 보여 주세요. 코드를 그대로 사용자에게 보여 주지 마세요.
consent_missingconsent_withdrawnattestation_faileddocument_expireddocument_unreadabledocument_not_supporteddocument_suspected_tamperingdocument_number_visiblechip_verification_failedchip_proof_missingchip_data_mismatchmrz_invalidphoto_qualityface_not_foundface_mismatchliveness_failedunderagelocation_not_supportedresidence_not_supportedsanctions_matchpep_reviewduplicate_identitypossible_duplicatedeclaration_requires_reviewuser_requested_reviewreviewer_otheraddress_unreadableaddress_document_too_oldaddress_document_not_acceptedaddress_name_mismatchaddress_signature_requiredfunds_document_unreadablefunds_document_too_oldfunds_document_not_acceptedfunds_more_evidencefunds_evidence_rejected