Petnow LogoPetnow

Registration/Verification/Identification

Pet fingerprint registration, verification (1:1), and identification (1:N) API guide.

Overview

Petnow API provides three core features:

FeatureDescriptionUse Case
RegistrationAdd fingerprints to petRegister new pet
Verification1:1 matchingConfirm "Is this the right pet?"
Identification1:N matchingFind "Who is this pet?"

Start a job only after every upload has finished

Call addFingerprints, verify, or identify only after every fingerprint and appearance upload for the session has returned 201. There is no separate session-end call, so this call tells the server that the session is complete. The job may not use an image that arrives after the call, and that upload still returns 201. If you upload in parallel, wait for all uploads to finish before you start the job.

Add Fingerprints (Registration)

Add fingerprints from a capture session to a pet.

Endpoint: POST /v2/pets/{petId}:addFingerprints

Recommended registration workflow: create the pet profile first (POST /v2/pets), then run the registration capture session for that petId and call addFingerprints. Registration does not run identification automatically, so the server does not prevent the same animal from being registered twice under different profiles. If your product must prevent duplicates, run an identification session first and decide in your own code before creating the profile.

Complete Flow

Request

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):
    # Request fingerprint addition
    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}")

    # Poll for job status
    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)  # Wait 2 seconds

# Usage example
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 };

  // Request fingerprint addition
  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}`);

  // Poll for job status
  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)); // Wait 2 seconds
  }
}

// Usage example
const result = await addFingerprints("pet-uuid-1234", "session-uuid-abcd");

Response (Job Started)

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

Job Status Response

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

The fingerprint-addition job status returns only status (PENDING, SUCCESS, or FAILED) — there are no petId or addedCount fields.

Pet Verification

Verify if captured biometric data matches a specific pet (1:1 matching).

Endpoint: POST /v2/pets/{petId}:verify

Complete Flow

Request

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):
    # Request verification
    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}")

    # Poll for job status
    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)

# Usage example
result = verify_pet("pet-uuid-1234", "session-uuid-abcd")
if result["isVerified"]:
    print("✅ Identity confirmed!")
else:
    print("❌ Does not match")
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 };

  // Request verification
  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}`);

  // Poll for job status
  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));
  }
}

// Usage example
const result = await verifyPet("pet-uuid-1234", "session-uuid-abcd");
if (result.isVerified) {
  console.log("✅ Identity confirmed!");
} else {
  console.log("❌ Does not match");
}

Verification Result Response

{
  "success": true,
  "data": {
    "status": "SUCCESS",
    "isVerified": true,
    "score": 95
  }
}
FieldTypeDescription
statusstringPENDING, SUCCESS, FAILED
isVerifiedbooleanWhether verification passed
scorenumberConfidence score (0-100)

Pet Identification

Find matching pets in the database from captured biometric data (1:N matching).

Search pool: the pets:identify endpoint always matches against the default search pool, which contains all of your registered pets. The identify request does not take a pool parameter. Segmenting pets into separate search pools is an advanced feature configured by your Petnow representative — contact us if you need it.

Endpoint: POST /v2/pets:identify

Plan gating: identification will require a plan that includes it (Petify Plus). An account without that entitlement will be answered with HTTP 402 and PETNOWB2B10011 (identification not in plan) or PETNOWB2B10012 (a plan must be selected first — accounts mid-migration from the legacy plan). Both codes may start being returned in a future release, so handle them now. Choose or upgrade your plan in the Petify Console; see Error Codes for the codes.

Complete Flow

Request

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):
    # Request identification
    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}")

    # Poll for job status
    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)

# Usage example
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 };

  // Request identification
  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}`);

  // Poll for job status
  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));
  }
}

// Usage example
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");
}

Identification Result Response

{
  "success": true,
  "data": {
    "status": "SUCCESS",
    "pets": [
      {
        "id": "pet-uuid-1234",
        "score": 95,
        "metadata": "{\"name\": \"Buddy\", \"owner\": \"John Doe\"}"
      },
      {
        "id": "pet-uuid-5678",
        "score": 78,
        "metadata": "{\"name\": \"Max\", \"owner\": \"Jane Smith\"}"
      }
    ]
  }
}
FieldTypeDescription
petsarrayMatched pets list (sorted by score descending)
pets[].idstringPet ID
pets[].scorenumberConfidence score (0-100)
pets[].metadatastringPet metadata (JSON string)

Interpreting scores

score is a similarity value from 0 to 100. The server applies its own match threshold to produce isVerified (verification) and the candidate list (identification) — you do not configure that threshold. A high score against a different pet can occur, especially with many similar-looking animals in the pool, so combine the score with your own context rather than treating it as proof on its own:

  • Show only the top N candidates (e.g. Top 3) instead of the full list.
  • Apply a minimum score that fits your business case (e.g. 70 or above) and treat lower candidates as "no confident match".
  • Pre-filter with your own data — species, breed, region, owner, or other metadata — before accepting a candidate.
  • Add a user confirmation step: show the candidate(s) and let the user confirm.

See Troubleshooting for capture-quality causes of low or noisy scores.

Job Status Values

All async jobs have the following statuses:

StatusDescription
PENDINGProcessing
SUCCESSCompleted successfully
FAILEDFailed

Result fields are present only on SUCCESS. isVerified/score (verification) and pets[] (identification) appear only in the SUCCESS response. The fingerprint-addition (registration) job returns only status with no additional result fields.
A FAILED response contains only status — there is no separate error/reason field. If the failure is due to a bad request, it is returned as an error response at job-submission time (see Error Codes); if a job fails asynchronously (e.g. insufficient image quality), capture again and retry.

Instead of polling, you can receive the result at your server as a signed event. See Webhooks.

ItemRecommended Value
Polling Interval2-3 seconds
Max Wait Time60 seconds
Max Retry Count20-30 times

Tip: Using exponential backoff can reduce server load and make polling more efficient. We recommend starting at 1 second and increasing up to 5 seconds maximum.

Next Steps

On this page