Petnow LogoPetnow
Server API

등록/검증/식별

반려동물 지문 등록, 검증(1:1), 식별(1:N) API를 안내합니다.

개요

Petnow API는 세 가지 핵심 기능을 제공합니다:

기능설명용도
등록 (Registration)펫에 지문 추가새 반려동물 등록
검증 (Verification)1:1 매칭"이 펫이 맞는지" 확인
식별 (Identification)1:N 매칭"이 펫이 누구인지" 찾기

모든 업로드가 끝난 뒤 작업을 시작하세요

addFingerprints, verify, identify는 그 세션의 지문·외관 이미지 업로드가 모두 201을 받은 뒤에 호출하세요. 별도의 세션 종료 API가 없으므로, 이 호출이 서버에 세션이 끝났다고 알리는 신호입니다. 호출 이후 도착한 이미지는 작업에 쓰이지 않을 수 있으며, 그 업로드도 201로 응답합니다. 업로드를 병렬로 보낸다면 모두 끝날 때까지 기다린 뒤 작업을 시작하세요.

지문 등록 (Add Fingerprints)

캡처 세션의 지문을 펫에 추가합니다.

엔드포인트: POST /v2/pets/{petId}:addFingerprints

권장 등록 워크플로우: 먼저 펫 프로필을 생성하고(POST /v2/pets), 그 petId로 등록용 캡처 세션을 진행한 뒤 addFingerprints를 호출하세요. 등록은 식별을 자동으로 실행하지 않으므로, 같은 동물이 서로 다른 프로필로 두 번 등록되는 것을 서버가 막지 않습니다. 중복 등록을 막아야 하는 제품이라면 프로필을 만들기 전에 식별 세션을 먼저 실행하고 고객사 코드에서 판단하세요.

전체 플로우

요청

curl -X POST "https://api.petify.petnow.io/v2/pets/pet-uuid-1234:addFingerprints" \
  -H "x-petnow-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "session-uuid-abcd"
  }'
import requests
import time

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.petify.petnow.io"
HEADERS = {"x-petnow-api-key": API_KEY}

def add_fingerprints(pet_id: str, session_id: str):
    # 지문 추가 요청
    response = requests.post(
        f"{BASE_URL}/v2/pets/{pet_id}:addFingerprints",
        headers={**HEADERS, "Content-Type": "application/json"},
        json={"sessionId": session_id}
    )
    job_id = response.json()["data"]["jobId"]
    print(f"Job started: {job_id}")

    # 작업 상태 폴링
    while True:
        status_response = requests.get(
            f"{BASE_URL}/v2/fingerprint-addition-jobs/{job_id}",
            headers=HEADERS
        )
        result = status_response.json()["data"]
        status = result["status"]
        
        print(f"Status: {status}")
        
        if status == "SUCCESS":
            print("Fingerprints added successfully!")
            return result
        elif status == "FAILED":
            raise Exception("Fingerprint addition job failed (status=FAILED)")
        
        time.sleep(2)  # 2초 대기

# 사용 예시
result = add_fingerprints("pet-uuid-1234", "session-uuid-abcd")
const API_KEY = "YOUR_API_KEY";
const BASE_URL = "https://api.petify.petnow.io";

async function addFingerprints(petId, sessionId) {
  const headers = { "x-petnow-api-key": API_KEY };

  // 지문 추가 요청
  const response = await fetch(
    `${BASE_URL}/v2/pets/${petId}:addFingerprints`,
    {
      method: "POST",
      headers: { ...headers, "Content-Type": "application/json" },
      body: JSON.stringify({ sessionId })
    }
  );
  const jobId = (await response.json()).data.jobId;
  console.log(`Job started: ${jobId}`);

  // 작업 상태 폴링
  while (true) {
    const statusResponse = await fetch(
      `${BASE_URL}/v2/fingerprint-addition-jobs/${jobId}`,
      { headers }
    );
    const result = (await statusResponse.json()).data;
    const status = result.status;
    
    console.log(`Status: ${status}`);
    
    if (status === "SUCCESS") {
      console.log("Fingerprints added successfully!");
      return result;
    } else if (status === "FAILED") {
      throw new Error("Fingerprint addition job failed (status=FAILED)");
    }
    
    await new Promise(resolve => setTimeout(resolve, 2000)); // 2초 대기
  }
}

// 사용 예시
const result = await addFingerprints("pet-uuid-1234", "session-uuid-abcd");

응답 (작업 시작)

{
  "success": true,
  "data": {
    "jobId": "job-uuid-1234"
  }
}

작업 상태 조회 응답

{
  "success": true,
  "data": {
    "status": "SUCCESS"
  }
}

지문 추가 작업 상태 응답은 status(PENDING, SUCCESS, FAILED) 하나만 반환합니다 — petId나 addedCount 필드는 없습니다.

펫 검증 (Verification)

캡처된 생체 정보가 특정 펫과 일치하는지 확인합니다 (1:1 매칭).

엔드포인트: POST /v2/pets/{petId}:verify

전체 플로우

요청

curl -X POST "https://api.petify.petnow.io/v2/pets/pet-uuid-1234:verify" \
  -H "x-petnow-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "session-uuid-abcd"
  }'
import requests
import time

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.petify.petnow.io"
HEADERS = {"x-petnow-api-key": API_KEY}

def verify_pet(pet_id: str, session_id: str):
    # 검증 요청
    response = requests.post(
        f"{BASE_URL}/v2/pets/{pet_id}:verify",
        headers={**HEADERS, "Content-Type": "application/json"},
        json={"sessionId": session_id}
    )
    job_id = response.json()["data"]["jobId"]
    print(f"Verification job started: {job_id}")

    # 작업 상태 폴링
    while True:
        status_response = requests.get(
            f"{BASE_URL}/v2/verification-jobs/{job_id}",
            headers=HEADERS
        )
        result = status_response.json()["data"]
        status = result["status"]
        
        if status == "SUCCESS":
            is_verified = result["isVerified"]
            score = result["score"]
            print(f"Verified: {is_verified}, Score: {score}")
            return result
        elif status == "FAILED":
            raise Exception("Verification job failed (status=FAILED)")
        
        time.sleep(2)

# 사용 예시
result = verify_pet("pet-uuid-1234", "session-uuid-abcd")
if result["isVerified"]:
    print("✅ 본인 확인 완료!")
else:
    print("❌ 일치하지 않음")
const API_KEY = "YOUR_API_KEY";
const BASE_URL = "https://api.petify.petnow.io";

async function verifyPet(petId, sessionId) {
  const headers = { "x-petnow-api-key": API_KEY };

  // 검증 요청
  const response = await fetch(
    `${BASE_URL}/v2/pets/${petId}:verify`,
    {
      method: "POST",
      headers: { ...headers, "Content-Type": "application/json" },
      body: JSON.stringify({ sessionId })
    }
  );
  const jobId = (await response.json()).data.jobId;
  console.log(`Verification job started: ${jobId}`);

  // 작업 상태 폴링
  while (true) {
    const statusResponse = await fetch(
      `${BASE_URL}/v2/verification-jobs/${jobId}`,
      { headers }
    );
    const result = (await statusResponse.json()).data;
    
    if (result.status === "SUCCESS") {
      console.log(`Verified: ${result.isVerified}, Score: ${result.score}`);
      return result;
    } else if (result.status === "FAILED") {
      throw new Error("Verification job failed (status=FAILED)");
    }
    
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
}

// 사용 예시
const result = await verifyPet("pet-uuid-1234", "session-uuid-abcd");
if (result.isVerified) {
  console.log("✅ 본인 확인 완료!");
} else {
  console.log("❌ 일치하지 않음");
}

검증 결과 응답

{
  "success": true,
  "data": {
    "status": "SUCCESS",
    "isVerified": true,
    "score": 95
  }
}
필드타입설명
statusstringPENDING, SUCCESS, FAILED
isVerifiedboolean일치 여부
scorenumber신뢰도 점수 (0-100)

펫 식별 (Identification)

데이터베이스에서 캡처된 생체 정보와 일치하는 펫을 찾습니다 (1:N 매칭).

검색 풀(search pool): pets:identify 엔드포인트는 항상 default 검색 풀을 대상으로 매칭하며, 이 풀에는 귀하가 등록한 전체 펫이 포함됩니다. 식별 요청은 풀 파라미터를 받지 않습니다. 펫을 여러 검색 풀로 분리하는 것은 Petnow 담당자가 구성하는 고급 기능이므로, 필요하면 문의해 주세요.

엔드포인트: POST /v2/pets:identify

플랜 제한: 식별에는 이를 포함하는 플랜(Petify Plus)이 필요해집니다. 권한이 없는 계정은 이 엔드포인트에서 HTTP 402와 함께 PETNOWB2B10011(플랜에 식별 미포함) 또는 PETNOWB2B10012(플랜을 먼저 선택해야 함 — 레거시 플랜에서 이전 중인 계정)를 받게 됩니다. 두 코드 모두 향후 릴리즈부터 반환될 수 있으므로 미리 처리해 두세요. 플랜 선택·업그레이드는 Petify Console에서 하며, 코드 상세는 에러 코드를 참고하세요.

전체 플로우

요청

curl -X POST "https://api.petify.petnow.io/v2/pets:identify" \
  -H "x-petnow-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "session-uuid-abcd"
  }'
import requests
import time

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.petify.petnow.io"
HEADERS = {"x-petnow-api-key": API_KEY}

def identify_pet(session_id: str):
    # 식별 요청
    response = requests.post(
        f"{BASE_URL}/v2/pets:identify",
        headers={**HEADERS, "Content-Type": "application/json"},
        json={"sessionId": session_id}
    )
    job_id = response.json()["data"]["jobId"]
    print(f"Identification job started: {job_id}")

    # 작업 상태 폴링
    while True:
        status_response = requests.get(
            f"{BASE_URL}/v2/identification-jobs/{job_id}",
            headers=HEADERS
        )
        result = status_response.json()["data"]
        status = result["status"]
        
        if status == "SUCCESS":
            pets = result.get("pets", [])
            print(f"Found {len(pets)} matching pet(s)")
            for pet in pets:
                print(f"  - {pet['id']}: score={pet['score']}")
            return result
        elif status == "FAILED":
            raise Exception("Identification job failed (status=FAILED)")
        
        time.sleep(2)

# 사용 예시
result = identify_pet("session-uuid-abcd")
if result["pets"]:
    best_match = result["pets"][0]
    print(f"Best match: {best_match['id']} (score: {best_match['score']})")
else:
    print("No matching pets found")
const API_KEY = "YOUR_API_KEY";
const BASE_URL = "https://api.petify.petnow.io";

async function identifyPet(sessionId) {
  const headers = { "x-petnow-api-key": API_KEY };

  // 식별 요청
  const response = await fetch(
    `${BASE_URL}/v2/pets:identify`,
    {
      method: "POST",
      headers: { ...headers, "Content-Type": "application/json" },
      body: JSON.stringify({ sessionId })
    }
  );
  const jobId = (await response.json()).data.jobId;
  console.log(`Identification job started: ${jobId}`);

  // 작업 상태 폴링
  while (true) {
    const statusResponse = await fetch(
      `${BASE_URL}/v2/identification-jobs/${jobId}`,
      { headers }
    );
    const result = (await statusResponse.json()).data;
    
    if (result.status === "SUCCESS") {
      const pets = result.pets || [];
      console.log(`Found ${pets.length} matching pet(s)`);
      pets.forEach(pet => {
        console.log(`  - ${pet.id}: score=${pet.score}`);
      });
      return result;
    } else if (result.status === "FAILED") {
      throw new Error("Identification job failed (status=FAILED)");
    }
    
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
}

// 사용 예시
const result = await identifyPet("session-uuid-abcd");
if (result.pets?.length > 0) {
  const bestMatch = result.pets[0];
  console.log(`Best match: ${bestMatch.id} (score: ${bestMatch.score})`);
} else {
  console.log("No matching pets found");
}

식별 결과 응답

{
  "success": true,
  "data": {
    "status": "SUCCESS",
    "pets": [
      {
        "id": "pet-uuid-1234",
        "score": 95,
        "metadata": "{\"name\": \"버디\", \"owner\": \"홍길동\"}"
      },
      {
        "id": "pet-uuid-5678",
        "score": 78,
        "metadata": "{\"name\": \"맥스\", \"owner\": \"김영희\"}"
      }
    ]
  }
}
필드타입설명
petsarray매칭된 펫 목록 (점수 내림차순)
pets[].idstring펫 ID
pets[].scorenumber신뢰도 점수 (0-100)
pets[].metadatastring펫 메타데이터 (JSON 문자열)

점수 해석하기

score는 0~100 범위의 유사도 값입니다. 서버는 자체 매칭 임계값을 적용해 isVerified(검증)와 후보 목록(식별)을 만들며, 이 임계값은 고객사가 설정하지 않습니다. 특히 풀에 외형이 비슷한 동물이 많으면 다른 펫에 높은 점수가 나올 수 있으므로, 점수만을 증거로 삼지 말고 고객사의 맥락과 함께 판단하세요:

  • 전체 목록 대신 상위 N개만 표시하세요 (예: Top 3).
  • 비즈니스 요건에 맞는 최소 점수를 적용하고(예: 70 이상), 그 아래 후보는 "확실한 매칭 없음"으로 처리하세요.
  • 후보를 채택하기 전에 종, 품종, 지역, 소유자 등 고객사 데이터로 사전 필터링하세요.
  • 사용자 확인 단계를 추가하세요: 후보를 보여주고 사용자가 확인하게 합니다.

촬영 품질로 인한 낮거나 불안정한 점수의 원인은 문제 해결을 참고하세요.

작업 상태 값

모든 비동기 작업은 다음 상태를 가집니다:

상태설명
PENDING처리 중
SUCCESS성공
FAILED실패

결과 필드는 SUCCESS일 때만 포함됩니다. 검증은 isVerified/score, 식별은 pets[]가 SUCCESS 응답에만 들어갑니다. 지문 추가(등록) 작업은 status만 반환하며 추가 결과 필드가 없습니다.
FAILED 응답에는 status만 있고 별도의 사유(error) 필드는 없습니다. 실패 원인이 잘못된 요청 때문이면 작업 시작 시점에 에러 응답(에러 코드)으로 반환되고, 작업이 비동기로 FAILED가 되는 경우(예: 품질 미달)는 재촬영 후 다시 시도하세요.

폴링 대신 결과를 서명된 이벤트로 서버에서 받을 수도 있습니다. Webhooks를 참고하세요.

권장 폴링 설정

항목권장값
폴링 간격2-3초
최대 대기 시간60초
최대 재시도 횟수20-30회

팁: 지수 백오프(exponential backoff)를 사용하면 서버 부하를 줄이고 효율적으로 폴링할 수 있습니다. 초기 간격 1초에서 시작하여 최대 5초까지 증가시키는 방식을 권장합니다.

다음 단계

On this page