`
# Biometric Upload
Source: /petify/docs/server/api/biometric
> Upload fingerprint (nose) and appearance images using the Petnow API.
## Overview [#overview]
This API allows you to upload pet biometric data (fingerprint/nose images, appearance images). You must create a capture session before uploading.
All calls on this page run **on your app server** with the `x-petnow-api-key` header. Do not call them directly from a client app (that would expose the API key). A mobile/RN client sends the captured `file://` images **to your app server**, which then uploads them via the endpoints below.
## Create Capture Session [#create-capture-session]
Create a capture session before uploading biometric data.
**Endpoint**: `POST /v2/capture-sessions`
### Request [#request]
```bash
curl -X POST "https://api.petify.petnow.io/v2/capture-sessions" \
-H "x-petnow-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"species": "DOG",
"purpose": "PET_PROFILE_REGISTRATION",
"petId": "pet-uuid-1234"
}'
```
```python
import requests
url = "https://api.petify.petnow.io/v2/capture-sessions"
headers = {
"x-petnow-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"species": "DOG",
"purpose": "PET_PROFILE_REGISTRATION",
"petId": "pet-uuid-1234"
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
session_id = result["data"]["id"]
print(f"Session ID: {session_id}")
```
```javascript
const response = await fetch("https://api.petify.petnow.io/v2/capture-sessions", {
method: "POST",
headers: {
"x-petnow-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
species: "DOG",
purpose: "PET_PROFILE_REGISTRATION",
petId: "pet-uuid-1234"
})
});
const result = await response.json();
const sessionId = result.data.id;
console.log(`Session ID: ${sessionId}`);
```
### Request Parameters [#request-parameters]
| Field | Type | Required | Description |
| --------- | ------ | ----------- | -------------------------------------- |
| `species` | string | ✅ | Species: `DOG` or `CAT` |
| `purpose` | string | ✅ | Purpose (see table below) |
| `petId` | string | Conditional | Required for registration/verification |
### Purpose Values [#purpose-values]
| Purpose | Description | Pet ID |
| -------------------------- | ------------------------- | ------------ |
| `PET_PROFILE_REGISTRATION` | Register new pet | Required |
| `PET_VERIFICATION` | Verify specific pet (1:1) | Required |
| `PET_IDENTIFICATION` | Identify pet (1:N) | Not required |
### Response [#response]
```json
{
"success": true,
"data": {
"id": "session-uuid-abcd"
}
}
```
## Upload Fingerprint (Nose) Image [#upload-fingerprint-nose-image]
Upload a pet's nose (fingerprint) image.
**Endpoint**: `POST /v2/fingerprints:upload?sessionId={sessionId}`
**Required**: The `sessionId` query parameter is always required. Uploaded files are automatically linked to the session.
### Request [#request-1]
```bash
curl -X POST "https://api.petify.petnow.io/v2/fingerprints:upload?sessionId=session-uuid-abcd" \
-H "x-petnow-api-key: YOUR_API_KEY" \
-F "file=@/path/to/nose_image.jpg"
```
```python
import requests
session_id = "session-uuid-abcd"
url = f"https://api.petify.petnow.io/v2/fingerprints:upload?sessionId={session_id}"
headers = {
"x-petnow-api-key": "YOUR_API_KEY"
}
with open("/path/to/nose_image.jpg", "rb") as f:
files = {"file": ("nose_image.jpg", f, "image/jpeg")}
response = requests.post(url, headers=headers, files=files)
result = response.json()
fingerprint_id = result["data"]["id"]
print(f"Fingerprint ID: {fingerprint_id}")
```
```javascript
const sessionId = "session-uuid-abcd";
const formData = new FormData();
formData.append("file", fileBlob, "nose_image.jpg");
const response = await fetch(
`https://api.petify.petnow.io/v2/fingerprints:upload?sessionId=${sessionId}`,
{
method: "POST",
headers: {
"x-petnow-api-key": "YOUR_API_KEY"
},
body: formData
}
);
const result = await response.json();
const fingerprintId = result.data.id;
console.log(`Fingerprint ID: ${fingerprintId}`);
```
### Request Parameters [#request-parameters-1]
| Field | Type | Required | Description |
| ----------- | -------------- | -------- | ------------------------------------ |
| `sessionId` | string (query) | ✅ | Capture session ID |
| `file` | file | ✅ | Fingerprint (nose) image file (JPEG) |
### Response [#response-1]
```json
{
"success": true,
"data": {
"id": "fingerprint-uuid-1234"
}
}
```
**Upload Count**: Minimum 5 fingerprint images for registration, minimum 3 for verification/identification. More images improve recognition accuracy. An appearance image is also **required** for every purpose — upload the appearance images the UI module provides; you do not need to track the count.
## Upload Appearance Image [#upload-appearance-image]
Upload the pet's appearance image. This is **required** for every capture session — registration/fingerprint addition, verification, and identification. Upload the appearance images the UI module provides; omitting them causes the action to fail with `CaptureSessionIsNotFinishedException` (`PETNOWB2B20012`).
**Endpoint**: `POST /v2/appearances:upload?sessionId={sessionId}`
### Request [#request-2]
```bash
curl -X POST "https://api.petify.petnow.io/v2/appearances:upload?sessionId=session-uuid-abcd" \
-H "x-petnow-api-key: YOUR_API_KEY" \
-F "file=@/path/to/pet_photo.jpg"
```
```python
import requests
session_id = "session-uuid-abcd"
url = f"https://api.petify.petnow.io/v2/appearances:upload?sessionId={session_id}"
headers = {
"x-petnow-api-key": "YOUR_API_KEY"
}
with open("/path/to/pet_photo.jpg", "rb") as f:
files = {"file": ("pet_photo.jpg", f, "image/jpeg")}
response = requests.post(url, headers=headers, files=files)
result = response.json()
appearance_id = result["data"]["id"]
print(f"Appearance ID: {appearance_id}")
```
```javascript
const sessionId = "session-uuid-abcd";
const formData = new FormData();
formData.append("file", fileBlob, "pet_photo.jpg");
const response = await fetch(
`https://api.petify.petnow.io/v2/appearances:upload?sessionId=${sessionId}`,
{
method: "POST",
headers: {
"x-petnow-api-key": "YOUR_API_KEY"
},
body: formData
}
);
const result = await response.json();
const appearanceId = result.data.id;
console.log(`Appearance ID: ${appearanceId}`);
```
### Request Parameters [#request-parameters-2]
| Field | Type | Required | Description |
| ----------- | -------------- | -------- | ---------------------------- |
| `sessionId` | string (query) | ✅ | Capture session ID |
| `file` | file | ✅ | Appearance image file (JPEG) |
### Response [#response-2]
```json
{
"success": true,
"data": {
"id": "appearance-uuid-5678"
}
}
```
## Complete Upload Flow Example [#complete-upload-flow-example]
**Automatic Session Linking**: When uploading with the `sessionId` query parameter, files are automatically linked to the session. No separate session end API call is required.
**Finish every upload before you start a job**: start registration, verification, or identification only after every upload below has returned `201`. The job may not use an image that arrives after the job call. If you upload in parallel, wait for all uploads to finish first. See [Registration/Verification/Identification](/petify/docs/server/api/identification).
```python
import requests
from pathlib import Path
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.petify.petnow.io"
HEADERS = {"x-petnow-api-key": API_KEY}
def upload_biometrics(pet_id: str, fingerprint_files: list, appearance_files: list):
# 1. Create capture session
session_response = requests.post(
f"{BASE_URL}/v2/capture-sessions",
headers={**HEADERS, "Content-Type": "application/json"},
json={
"species": "DOG",
"purpose": "PET_PROFILE_REGISTRATION",
"petId": pet_id
}
)
session_id = session_response.json()["data"]["id"]
print(f"Created session: {session_id}")
# 2. Upload fingerprints (auto-linked via sessionId query parameter)
fingerprint_ids = []
for file_path in fingerprint_files:
with open(file_path, "rb") as f:
response = requests.post(
f"{BASE_URL}/v2/fingerprints:upload?sessionId={session_id}",
headers=HEADERS,
files={"file": (Path(file_path).name, f, "image/jpeg")}
)
fp_id = response.json()["data"]["id"]
fingerprint_ids.append(fp_id)
print(f"Uploaded fingerprint: {fp_id}")
# 3. Upload appearances (auto-linked via sessionId query parameter)
appearance_ids = []
for file_path in appearance_files:
with open(file_path, "rb") as f:
response = requests.post(
f"{BASE_URL}/v2/appearances:upload?sessionId={session_id}",
headers=HEADERS,
files={"file": (Path(file_path).name, f, "image/jpeg")}
)
ap_id = response.json()["data"]["id"]
appearance_ids.append(ap_id)
print(f"Uploaded appearance: {ap_id}")
return session_id, fingerprint_ids, appearance_ids
# Usage example
session_id, fp_ids, ap_ids = upload_biometrics(
pet_id="pet-uuid-1234",
fingerprint_files=["nose1.jpg", "nose2.jpg", "nose3.jpg", "nose4.jpg", "nose5.jpg"],
appearance_files=["full_body.jpg"]
)
```
```javascript
const API_KEY = "YOUR_API_KEY";
const BASE_URL = "https://api.petify.petnow.io";
async function uploadBiometrics(petId, fingerprintFiles, appearanceFiles) {
const headers = { "x-petnow-api-key": API_KEY };
// 1. Create capture session
const sessionResponse = await fetch(`${BASE_URL}/v2/capture-sessions`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
species: "DOG",
purpose: "PET_PROFILE_REGISTRATION",
petId: petId
})
});
const sessionId = (await sessionResponse.json()).data.id;
console.log(`Created session: ${sessionId}`);
// 2. Upload fingerprints (auto-linked via sessionId query parameter)
const fingerprintIds = [];
for (const file of fingerprintFiles) {
const formData = new FormData();
formData.append("file", file);
const response = await fetch(
`${BASE_URL}/v2/fingerprints:upload?sessionId=${sessionId}`,
{
method: "POST",
headers: headers,
body: formData
}
);
const fpId = (await response.json()).data.id;
fingerprintIds.push(fpId);
console.log(`Uploaded fingerprint: ${fpId}`);
}
// 3. Upload appearances (auto-linked via sessionId query parameter)
const appearanceIds = [];
for (const file of appearanceFiles) {
const formData = new FormData();
formData.append("file", file);
const response = await fetch(
`${BASE_URL}/v2/appearances:upload?sessionId=${sessionId}`,
{
method: "POST",
headers: headers,
body: formData
}
);
const apId = (await response.json()).data.id;
appearanceIds.push(apId);
console.log(`Uploaded appearance: ${apId}`);
}
return { sessionId, fingerprintIds, appearanceIds };
}
```
## Next Steps [#next-steps]
* [Registration/Verification/Identification](/petify/docs/server/api/identification)
* [Error Codes](/petify/docs/server/api/error-codes)
# Error Codes
Source: /petify/docs/server/api/error-codes
> Petnow Server API error codes and solutions guide.
## Overview [#overview]
Petnow API returns detailed error information along with HTTP status codes when an error occurs.
## Error Response Format [#error-response-format]
```json
{
"errors": [
{
"code": "PETNOWB2B10000"
}
]
}
```
| Field | Type | Description |
| --------------- | ------ | ---------------------------------- |
| `errors` | array | Error array |
| `errors[].code` | string | Error code (PETNOWB2BXXXXX format) |
## HTTP Status Codes [#http-status-codes]
| Status Code | Description |
| ----------- | ---------------------------------------------------------------------------- |
| `200` | Success |
| `201` | Resource created successfully |
| `202` | Accepted — async job started (addFingerprints / verify / identify) |
| `204` | Deletion successful (no response body) |
| `400` | Bad request |
| `401` | Authentication failed |
| `402` | Payment required |
| `403` | Permission denied |
| `404` | Resource not found |
| `409` | Conflict (Web Integration: `externalPetId` already registered) |
| `410` | Gone (Web Integration: result code already consumed) |
| `500` | Internal server error |
| `503` | Service unavailable (Web Integration: session issuance temporarily disabled) |
## Authentication Errors (401) [#authentication-errors-401]
### PETNOWB2B10000 - InvalidApiKeyException [#petnowb2b10000---invalidapikeyexception]
**Status Code**: 401
```json
{
"errors": [
{
"code": "PETNOWB2B10000"
}
]
}
```
**Cause**
* Invalid API Key
* Missing API Key
* Deactivated API Key
**Solution**
1. Check `x-petnow-api-key` header
2. Use the correct API Key issued from the [Petify Console](/petify/docs/petify-console)
3. Verify the key is active (and re-issue it if needed) in the [Petify Console](/petify/docs/petify-console) under My Page → API Key Management
### PETNOWB2B10001 - ValidationError [#petnowb2b10001---validationerror]
**Status Code**: 400
This error occurs when request parameter validation fails. Unlike other error codes, it provides specific validation error details via the `details` field.
```json
{
"errors": [
{
"code": "PETNOWB2B10001",
"details": {
"petId": ["This field is required."]
}
}
]
}
```
**Cause**
* Missing required parameters
* Invalid type or format for parameter values
* Disallowed enum values (e.g., a value other than `DOG` or `CAT` for `species`)
**Solution**
1. Check the `details` field for the failed field names and error messages
2. Fix the request parameters and retry
## Payment Required Errors (402) [#payment-required-errors-402]
### PETNOWB2B10005 - PetLimitExceededError [#petnowb2b10005---petlimitexceedederror]
**Status Code**: 402
```json
{
"errors": [
{
"code": "PETNOWB2B10005"
}
]
}
```
**Cause**
* Demo plan maximum pet registration count exceeded
**Solution**
1. Upgrade to a paid plan yourself in the [Petify Console](/petify/docs/petify-console) (My Page → Billing)
### PETNOWB2B10011 - IdentificationNotInPlanError [#petnowb2b10011---identificationnotinplanerror]
**Status Code**: 402
```json
{
"errors": [
{
"code": "PETNOWB2B10011"
}
]
}
```
**Cause**
* The tenant's plan does not include identification (1:N matching). This is returned by `POST /v2/pets:identify` and by `POST /v2/hosted-sessions` with `action: IDENTIFY` for accounts that are not entitled to identification (Petify Core).
* This code may start being returned in a future release. Handle it now so the change is a non-event.
**Solution**
1. Upgrade to a plan that includes identification (Petify Plus) in the [Petify Console](/petify/docs/petify-console#6-plans-and-billing) (My Page → Billing)
### PETNOWB2B10012 - IdentificationPlanSelectionRequiredError [#petnowb2b10012---identificationplanselectionrequirederror]
**Status Code**: 402
```json
{
"errors": [
{
"code": "PETNOWB2B10012"
}
]
}
```
**Cause**
* The account is being migrated from the legacy plan onto Petify Core / Plus and its migration date has passed with no plan chosen, so identification requests are blocked until a plan is selected. Distinct from `PETNOWB2B10011`: here **selecting either plan** resolves it.
* This code may start being returned in a future release. Handle it now.
**Solution**
1. Choose a plan in the [Petify Console](/petify/docs/petify-console#6-plans-and-billing) (My Page → Billing). Selecting either Core or Plus clears this state; identification itself additionally requires Plus.
## Forbidden Errors (403) [#forbidden-errors-403]
### PETNOWB2B10010 - Account API access suspended [#petnowb2b10010---account-api-access-suspended]
**Status Code**: 403
```json
{
"errors": [
{
"code": "PETNOWB2B10010"
}
]
}
```
**Cause**
* The account's API access is suspended due to a billing/payment-method issue. This is an account-level state, not an API Key problem.
**Solution**
1. Resolve the billing/payment-method issue in the [Petify Console](/petify/docs/petify-console).
## Request Errors (400) [#request-errors-400]
### PETNOWB2B20007 - PetAlreadyHasFingerprintAdditionJobException [#petnowb2b20007---petalreadyhasfingerprintadditionjobexception]
**Status Code**: 400
```json
{
"errors": [
{
"code": "PETNOWB2B20007"
}
]
}
```
**Cause**
* Pet already has an ongoing fingerprint addition job
**Solution**
1. Wait for the existing job to complete
2. Retry after polling job status
### PETNOWB2B20009 - PetHasNoFingerprintException [#petnowb2b20009---pethasnofingerprintexception]
**Status Code**: 400
```json
{
"errors": [
{
"code": "PETNOWB2B20009"
}
]
}
```
**Cause**
* Pet has no registered fingerprints during verification
**Solution**
1. Complete fingerprint registration first
2. Add fingerprints via `addFingerprints` endpoint
### PETNOWB2B20011 - NoPetIdCaptureSessionException [#petnowb2b20011---nopetidcapturesessionexception]
**Status Code**: 400
```json
{
"errors": [
{
"code": "PETNOWB2B20011"
}
]
}
```
**Cause**
* Missing `petId` when creating session with `PET_PROFILE_REGISTRATION` or `PET_VERIFICATION` purpose
**Solution**
1. Include `petId` parameter when creating session
2. `PET_IDENTIFICATION` does not require `petId`
### PETNOWB2B20012 - CaptureSessionIsNotFinishedException [#petnowb2b20012---capturesessionisnotfinishedexception]
**Status Code**: 400
```json
{
"errors": [
{
"code": "PETNOWB2B20012"
}
]
}
```
**Cause**
* No fingerprint image is uploaded to the session
* No appearance image is uploaded to the session (required for every purpose)
**Solution**
1. Upload the fingerprint images (minimum 5 for registration, 3 for verification/identification)
2. Upload the appearance image(s) the UI module provides
3. Start the action only after every upload has returned `201`
### PETNOWB2B20013 - InvalidSessionPurposeError [#petnowb2b20013---invalidsessionpurposeerror]
**Status Code**: 400
```json
{
"errors": [
{
"code": "PETNOWB2B20013"
}
]
}
```
**Cause**
* Capture session purpose does not match the requested operation
* Example: Attempting fingerprint registration with a `PET_IDENTIFICATION` session
**Solution**
1. Create a session with the correct purpose
2. Choose the appropriate purpose among `PET_PROFILE_REGISTRATION`, `PET_VERIFICATION`, `PET_IDENTIFICATION`
### PETNOWB2B20014 - CaptureSessionAlreadyUsedError [#petnowb2b20014---capturesessionalreadyusederror]
**Status Code**: 400
```json
{
"errors": [
{
"code": "PETNOWB2B20014"
}
]
}
```
**Cause**
* Attempting to reuse a capture session that has already been used
**Solution**
1. Create a new capture session and retry
### PETNOWB2B20015 - SpeciesMismatchError [#petnowb2b20015---speciesmismatcherror]
**Status Code**: 400
```json
{
"errors": [
{
"code": "PETNOWB2B20015"
}
]
}
```
**Cause**
* Capture session `species` does not match the pet's `species`
**Solution**
1. Create a session with the same `species` as the pet
2. Verify the correct pet ID
### PETNOWB2B20017 - MultipleCaptureSessionsError [#petnowb2b20017---multiplecapturesessionserror]
**Status Code**: 400
```json
{
"errors": [
{
"code": "PETNOWB2B20017"
}
]
}
```
**Cause**
* V1 fingerprint UUIDs belong to more than one capture session
**Solution**
1. Use fingerprint UUIDs from a single capture session only
## Session Errors [#session-errors]
### PETNOWB2B20010 - NoSuchCaptureSessionException [#petnowb2b20010---nosuchcapturesessionexception]
**Status Code**: 404
```json
{
"errors": [
{
"code": "PETNOWB2B20010"
}
]
}
```
**Cause**
* Session ID does not exist
* Session has expired
**Solution**
1. Verify session ID
2. Create a new session
## Resource Errors (404) [#resource-errors-404]
### PETNOWB2B20000 - NoSuchPetException [#petnowb2b20000---nosuchpetexception]
**Status Code**: 404
```json
{
"errors": [
{
"code": "PETNOWB2B20000"
}
]
}
```
**Cause**
* Pet ID does not exist
* Pet has been deleted
**Solution**
1. Verify pet ID
2. Check existence via pet list endpoint
### PETNOWB2B20004 - NoSuchFingerprintAdditionJobException [#petnowb2b20004---nosuchfingerprintadditionjobexception]
**Status Code**: 404
```json
{
"errors": [
{
"code": "PETNOWB2B20004"
}
]
}
```
**Cause**
* Fingerprint addition job ID does not exist
**Solution**
1. Verify job ID
2. Start a new job
### PETNOWB2B20005 - NoSuchPetVerificationJobException [#petnowb2b20005---nosuchpetverificationjobexception]
**Status Code**: 404
```json
{
"errors": [
{
"code": "PETNOWB2B20005"
}
]
}
```
**Cause**
* Verification job ID does not exist
**Solution**
1. Verify job ID
2. Start a new verification job
### PETNOWB2B20006 - NoSuchPetIdentificationJobException [#petnowb2b20006---nosuchpetidentificationjobexception]
**Status Code**: 404
```json
{
"errors": [
{
"code": "PETNOWB2B20006"
}
]
}
```
**Cause**
* Identification job ID does not exist
**Solution**
1. Verify job ID
2. Start a new identification job
### PETNOWB2B20016 - NoSuchSearchPoolError [#petnowb2b20016---nosuchsearchpoolerror]
**Status Code**: 404
```json
{
"errors": [
{
"code": "PETNOWB2B20016"
}
]
}
```
**Cause**
* Search pool name does not exist
**Solution**
1. Verify the correct search pool name
2. Contact Petnow representative to configure the search pool
## Web Integration Errors (Hosted Sessions) [#web-integration-errors-hosted-sessions]
Returned by the [Server Integration](/petify/docs/server/web-integration/server-integration) endpoints (`/v2/hosted-sessions`, `/v2/hosted-results`). They share the `{ "errors": [{ "code": "..." }] }` shape; `PETNOWB2B10000`, `PETNOWB2B10001`, `PETNOWB2B10005`, `PETNOWB2B10010`, `PETNOWB2B10011`, `PETNOWB2B10012` and `PETNOWB2B20015` can also be returned there with the meanings above.
| Code | HTTP | Cause | Solution |
| ---------------- | ---- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `PETNOWB2B21001` | 409 | `REGISTER`: the `externalPetId` already has an active registration | Use a different ID, or delete the existing pet in the Console first |
| `PETNOWB2B21002` | 404 | `VERIFY`: the `externalPetId` has no active registration | Register the pet through Web Integration first (SDK / Server API pets have no binding) |
| `PETNOWB2B21003` | 400 | `redirectUrl` is not on the allow-list (exact match) | Pre-register the URL under Integration → Links → Allowed Redirect URLs |
| `PETNOWB2B21004` | 404 | Hosted session not found | Check the session `id` you stored at issuance |
| `PETNOWB2B21005` | 404 | Result code unknown, expired, or of the wrong tier (a Link Integration display code on the `/v2` route) | Fall back to `GET /v2/hosted-sessions/{id}` |
| `PETNOWB2B21006` | 410 | Result code already consumed and the grace window has elapsed | Read the session via `GET /v2/hosted-sessions/{id}` |
## Server Errors (500) [#server-errors-500]
### PETNOWB2B30002 - UnknownSpeciesException [#petnowb2b30002---unknownspeciesexception]
**Status Code**: 500
```json
{
"errors": [
{
"code": "PETNOWB2B30002"
}
]
}
```
**Cause**
* Unknown pet species
**Solution**
1. Verify `species` value is `DOG` or `CAT`
## Error Handling Recommendations [#error-handling-recommendations]
### Retry Logic [#retry-logic]
```python
import time
import requests
def api_request_with_retry(url, method="GET", max_retries=3, **kwargs):
for attempt in range(max_retries):
try:
response = requests.request(method, url, **kwargs)
if response.status_code >= 500:
time.sleep(2 ** attempt) # Exponential backoff
continue
return response
except requests.exceptions.RequestException as e:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt)
return response
```
### Error Code Handling [#error-code-handling]
```javascript
async function handleApiError(response) {
const body = await response.json();
const errorCode = body.errors?.[0]?.code;
switch (errorCode) {
case "PETNOWB2B10000":
// Authentication error: Check API key
console.error("Authentication error: Invalid API key");
break;
case "PETNOWB2B10001":
// Validation error: Check details field for failed fields
console.error("Validation error:", body.errors?.[0]?.details);
break;
case "PETNOWB2B10005":
// Payment required: Pet registration limit exceeded
console.warn("Pet limit exceeded. Please upgrade your plan.");
break;
case "PETNOWB2B10011":
// Payment required: identification not included in the current plan
console.warn("Identification is not in your plan. Please upgrade.");
break;
case "PETNOWB2B10012":
// Payment required: no plan selected yet (choosing any plan clears this)
console.warn("Please select a plan to enable identification.");
break;
case "PETNOWB2B20000":
case "PETNOWB2B20010":
// Resource not found: Need to recreate
console.warn("Resource not found");
break;
default:
console.error("API error:", errorCode);
}
}
```
## Support Request [#support-request]
If errors persist, contact Petnow representative with the following information:
**Required Information**
* Error code and message
* HTTP status code
* Request URL and parameters
* Timestamp (UTC)
* API Key (masked)
**Contact**
* Email: [support@petnow.io](mailto:support@petnow.io)
# Registration/Verification/Identification
Source: /petify/docs/server/api/identification
> Pet fingerprint registration, verification (1:1), and identification (1:N) API guide.
## Overview [#overview]
Petnow API provides three core features:
| Feature | Description | Use Case |
| ------------------ | ----------------------- | -------------------------------- |
| **Registration** | Add fingerprints to pet | Register new pet |
| **Verification** | 1:1 matching | Confirm "Is this the right pet?" |
| **Identification** | 1:N matching | Find "Who is this pet?" |
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-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 [#complete-flow]
### Request [#request]
```bash
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"
}'
```
```python
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")
```
```javascript
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) [#response-job-started]
```json
{
"success": true,
"data": {
"jobId": "job-uuid-1234"
}
}
```
### Job Status Response [#job-status-response]
```json
{
"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 [#pet-verification]
Verify if captured biometric data matches a specific pet (1:1 matching).
**Endpoint**: `POST /v2/pets/{petId}:verify`
### Complete Flow [#complete-flow-1]
### Request [#request-1]
```bash
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"
}'
```
```python
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")
```
```javascript
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 [#verification-result-response]
```json
{
"success": true,
"data": {
"status": "SUCCESS",
"isVerified": true,
"score": 95
}
}
```
| Field | Type | Description |
| ------------ | ------- | ------------------------------ |
| `status` | string | `PENDING`, `SUCCESS`, `FAILED` |
| `isVerified` | boolean | Whether verification passed |
| `score` | number | Confidence score (0-100) |
## Pet Identification [#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](/petify/docs/petify-console#6-plans-and-billing); see [Error Codes](/petify/docs/server/api/error-codes) for the codes.
### Complete Flow [#complete-flow-2]
### Request [#request-2]
```bash
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"
}'
```
```python
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")
```
```javascript
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 [#identification-result-response]
```json
{
"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\"}"
}
]
}
}
```
| Field | Type | Description |
| ----------------- | ------ | ---------------------------------------------- |
| `pets` | array | Matched pets list (sorted by score descending) |
| `pets[].id` | string | Pet ID |
| `pets[].score` | number | Confidence score (0-100) |
| `pets[].metadata` | string | Pet metadata (JSON string) |
## Interpreting scores [#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](/petify/docs/sdk/troubleshooting) for capture-quality causes of low or noisy scores.
## Job Status Values [#job-status-values]
All async jobs have the following statuses:
| Status | Description |
| --------- | ---------------------- |
| `PENDING` | Processing |
| `SUCCESS` | Completed successfully |
| `FAILED` | Failed |
**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](/petify/docs/server/api/error-codes)); if a job fails asynchronously (e.g. insufficient image quality), capture again and retry.
## Recommended Polling Settings [#recommended-polling-settings]
| Item | Recommended Value |
| ---------------- | ----------------- |
| Polling Interval | 2-3 seconds |
| Max Wait Time | 60 seconds |
| Max Retry Count | 20-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 [#next-steps]
* [Error Codes](/petify/docs/server/api/error-codes)
# Server API Overview
Source: /petify/docs/server/api/overview
> Learn how to use the Petnow Server API and authentication methods.
## Overview [#overview]
Petnow Server API is a RESTful API that allows you to implement pet biometric recognition features directly from your server. We recommend calling the API from your server instead of using the mobile SDK's API Module.
**Why use Server API?**
* Securely manage API Keys on your server
* Handle requests independently of client app network issues
* Centralize business logic and data processing on your server
## Base URL [#base-url]
| Environment | URL |
| ----------- | ------------------------------ |
| Production | `https://api.petify.petnow.io` |
API Documentation (Redoc): [https://api.petify.petnow.io/redoc](https://api.petify.petnow.io/redoc)
## Authentication [#authentication]
All API requests require the `x-petnow-api-key` header.
```bash
curl -X GET "https://api.petify.petnow.io/v2/pets" \
-H "x-petnow-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json"
```
```python
import requests
headers = {
"x-petnow-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
response = requests.get(
"https://api.petify.petnow.io/v2/pets",
headers=headers
)
```
```javascript
const response = await fetch("https://api.petify.petnow.io/v2/pets", {
method: "GET",
headers: {
"x-petnow-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
});
```
### API Key Issuance [#api-key-issuance]
Issue your API Key directly from the [Petify Console](/petify/docs/petify-console) under My Page → API Key Management (a registered payment method is required first). See the [Petify Console](/petify/docs/petify-console) doc for sign-up and key issuance.
For questions, contact **[support@petnow.io](mailto:support@petnow.io)**.
Keep your API Key on the server only. Do not include it in client apps. If your API Key is exposed, contact us immediately for reissuance.
## Capture Session Concept [#capture-session-concept]
API v2 uses the **Capture Session** concept. The entire process of uploading and processing biometric data is managed through sessions.
### Session Lifecycle [#session-lifecycle]
### Session States [#session-states]
| State | Description |
| ---------- | ---------------------------------------------- |
| `STARTED` | Session is active and ready to receive uploads |
| `FINISHED` | Session successfully ended with captured data |
| `ABORTED` | Session was cancelled or failed |
| `TIMEOUT` | Session expired without proper closure |
### Session Purpose [#session-purpose]
| Purpose | Description | Pet ID Required |
| -------------------------- | ------------------------- | --------------- |
| `PET_PROFILE_REGISTRATION` | Register new pet | ✅ Yes |
| `PET_VERIFICATION` | Verify specific pet (1:1) | ✅ Yes |
| `PET_IDENTIFICATION` | Identify pet (1:N) | ❌ No |
## Complete Workflows [#complete-workflows]
### Pet Registration Flow [#pet-registration-flow]
### Pet Verification Flow [#pet-verification-flow]
### Pet Identification Flow [#pet-identification-flow]
## API Endpoint Summary [#api-endpoint-summary]
### Capture Sessions [#capture-sessions]
| Method | Endpoint | Description |
| ------ | ---------------------- | ---------------------- |
| POST | `/v2/capture-sessions` | Create capture session |
### Pet Management [#pet-management]
| Method | Endpoint | Description |
| ------ | ------------------ | --------------- |
| POST | `/v2/pets` | Create pet |
| GET | `/v2/pets` | List pets |
| GET | `/v2/pets/{petId}` | Get pet details |
| PATCH | `/v2/pets/{petId}` | Update pet |
| DELETE | `/v2/pets/{petId}` | Delete pet |
### Biometric Upload [#biometric-upload]
| Method | Endpoint | Description |
| ------ | ----------------------------------------------- | ------------------------------- |
| POST | `/v2/fingerprints:upload?sessionId={sessionId}` | Upload fingerprint (nose) image |
| POST | `/v2/appearances:upload?sessionId={sessionId}` | Upload appearance image |
### Verification & Identification [#verification--identification]
| Method | Endpoint | Description |
| ------ | ---------------------------------- | ---------------- |
| POST | `/v2/pets/{petId}:addFingerprints` | Add fingerprints |
| POST | `/v2/pets/{petId}:verify` | Verify pet |
| POST | `/v2/pets:identify` | Identify pet |
### Job Status [#job-status]
| Method | Endpoint | Description |
| ------ | --------------------------------------- | --------------------------- |
| GET | `/v2/fingerprint-addition-jobs/{jobId}` | Fingerprint addition status |
| GET | `/v2/verification-jobs/{jobId}` | Verification status |
| GET | `/v2/identification-jobs/{jobId}` | Identification status |
## Next Steps [#next-steps]
* [Pet Management API](/petify/docs/server/api/pet-management)
* [Biometric Upload](/petify/docs/server/api/biometric)
* [Registration/Verification/Identification](/petify/docs/server/api/identification)
* [Error Codes](/petify/docs/server/api/error-codes)
# Pet Management API
Source: /petify/docs/server/api/pet-management
> Create, read, update, and delete pet profiles using the Petnow API.
## Overview [#overview]
The Pet Management API allows you to perform CRUD (Create, Read, Update, Delete) operations on pet profiles.
## Create Pet [#create-pet]
Create a new pet profile.
**Endpoint**: `POST /v2/pets`
### Request [#request]
```bash
curl -X POST "https://api.petify.petnow.io/v2/pets" \
-H "x-petnow-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"species": "DOG",
"breed": "Golden Retriever",
"metadata": "{\"name\": \"Buddy\", \"age\": 3, \"owner\": \"John Doe\"}"
}'
```
```python
import requests
import json
url = "https://api.petify.petnow.io/v2/pets"
headers = {
"x-petnow-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"species": "DOG",
"breed": "Golden Retriever",
"metadata": json.dumps({
"name": "Buddy",
"age": 3,
"owner": "John Doe"
})
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
print(result)
# {"success": true, "data": {"id": "pet-uuid-1234"}}
```
```javascript
const response = await fetch("https://api.petify.petnow.io/v2/pets", {
method: "POST",
headers: {
"x-petnow-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
species: "DOG",
breed: "Golden Retriever",
metadata: JSON.stringify({
name: "Buddy",
age: 3,
owner: "John Doe"
})
})
});
const result = await response.json();
console.log(result);
// {success: true, data: {id: "pet-uuid-1234"}}
```
### Request Parameters [#request-parameters]
| Field | Type | Required | Description |
| ---------- | ------ | -------- | ------------------------------ |
| `species` | string | ✅ | Species: `DOG` or `CAT` |
| `breed` | string | ❌ | Breed |
| `metadata` | string | ❌ | Additional info as JSON string |
### Response [#response]
```json
{
"success": true,
"data": {
"id": "pet-uuid-1234"
}
}
```
**Using Metadata**: You can store additional information such as name, age, and owner details in the `metadata` field as a JSON string.
## List Pets [#list-pets]
Retrieve all registered pets.
**Endpoint**: `GET /v2/pets`
### Request [#request-1]
```bash
curl -X GET "https://api.petify.petnow.io/v2/pets" \
-H "x-petnow-api-key: YOUR_API_KEY"
```
```python
import requests
url = "https://api.petify.petnow.io/v2/pets"
headers = {
"x-petnow-api-key": "YOUR_API_KEY"
}
response = requests.get(url, headers=headers)
result = response.json()
print(result)
```
```javascript
const response = await fetch("https://api.petify.petnow.io/v2/pets", {
method: "GET",
headers: {
"x-petnow-api-key": "YOUR_API_KEY"
}
});
const result = await response.json();
console.log(result);
```
### Response [#response-1]
```json
{
"success": true,
"data": [
{
"id": "pet-uuid-1234",
"species": "DOG",
"breed": "Golden Retriever",
"metadata": "{\"name\": \"Buddy\", \"age\": 3}",
"hasFingerprint": true,
"createdAt": "2026-01-15T10:30:00Z"
},
{
"id": "pet-uuid-5678",
"species": "CAT",
"breed": "Russian Blue",
"metadata": "{\"name\": \"Nabi\", \"age\": 2}",
"hasFingerprint": false,
"createdAt": "2026-01-10T14:20:00Z"
}
]
}
```
`data` is the pet array directly (not wrapped in a `pets` field). Each item includes `hasFingerprint` (a boolean indicating whether the pet has any registered fingerprints).
## Get Pet Details [#get-pet-details]
Retrieve detailed information about a specific pet.
**Endpoint**: `GET /v2/pets/{petId}`
### Request [#request-2]
```bash
curl -X GET "https://api.petify.petnow.io/v2/pets/pet-uuid-1234" \
-H "x-petnow-api-key: YOUR_API_KEY"
```
```python
import requests
pet_id = "pet-uuid-1234"
url = f"https://api.petify.petnow.io/v2/pets/{pet_id}"
headers = {
"x-petnow-api-key": "YOUR_API_KEY"
}
response = requests.get(url, headers=headers)
result = response.json()
print(result)
```
```javascript
const petId = "pet-uuid-1234";
const response = await fetch(`https://api.petify.petnow.io/v2/pets/${petId}`, {
method: "GET",
headers: {
"x-petnow-api-key": "YOUR_API_KEY"
}
});
const result = await response.json();
console.log(result);
```
### Response [#response-2]
```json
{
"success": true,
"data": {
"id": "pet-uuid-1234",
"species": "DOG",
"breed": "Golden Retriever",
"metadata": "{\"name\": \"Buddy\", \"age\": 3, \"owner\": \"John Doe\"}",
"hasFingerprint": true,
"createdAt": "2026-01-15T10:30:00Z"
}
}
```
### Response Fields [#response-fields]
| Field | Type | Description |
| ---------------- | ------- | ----------------------------------------------- |
| `id` | string | Pet ID |
| `species` | string | `DOG` or `CAT` |
| `breed` | string | Breed |
| `metadata` | string | Additional info as JSON string |
| `hasFingerprint` | boolean | Whether the pet has any registered fingerprints |
| `createdAt` | string | Creation timestamp (ISO 8601) |
## Update Pet [#update-pet]
Update a pet's metadata.
**Endpoint**: `PATCH /v2/pets/{petId}`
### Request [#request-3]
```bash
curl -X PATCH "https://api.petify.petnow.io/v2/pets/pet-uuid-1234" \
-H "x-petnow-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"breed": "Golden Retriever Mix",
"metadata": "{\"name\": \"Buddy\", \"age\": 4, \"owner\": \"John Doe\", \"vaccinated\": true}"
}'
```
```python
import requests
import json
pet_id = "pet-uuid-1234"
url = f"https://api.petify.petnow.io/v2/pets/{pet_id}"
headers = {
"x-petnow-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
data = {
"breed": "Golden Retriever Mix",
"metadata": json.dumps({
"name": "Buddy",
"age": 4,
"owner": "John Doe",
"vaccinated": True
})
}
response = requests.patch(url, headers=headers, json=data)
result = response.json()
print(result)
```
```javascript
const petId = "pet-uuid-1234";
const response = await fetch(`https://api.petify.petnow.io/v2/pets/${petId}`, {
method: "PATCH",
headers: {
"x-petnow-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
breed: "Golden Retriever Mix",
metadata: JSON.stringify({
name: "Buddy",
age: 4,
owner: "John Doe",
vaccinated: true
})
})
});
const result = await response.json();
console.log(result);
```
### Request Parameters [#request-parameters-1]
| Field | Type | Required | Description |
| ---------- | ------ | -------- | ------------------------------ |
| `metadata` | string | ✅ | Additional info as JSON string |
| `breed` | string | ❌ | Breed |
### Response [#response-3]
```json
{
"success": true,
"data": {
"isSuccessful": true
}
}
```
Update returns only `{ "isSuccessful": true }` — it does **not** echo back the updated pet. Call [Get Pet Details](#get-pet-details) afterwards if you need the updated values.
## Delete Pet [#delete-pet]
Delete a pet profile and all associated biometric data. Deleting a pet removes **the pet profile, all registered fingerprints, and all appearance images**. The pet is **excluded from verification and identification immediately**, and it no longer counts as a registered pet for billing. To use the service for that pet again, it must be **re-registered** (a new profile and a new capture).
**Endpoint**: `DELETE /v2/pets/{petId}`
### Request [#request-4]
```bash
curl -X DELETE "https://api.petify.petnow.io/v2/pets/pet-uuid-1234" \
-H "x-petnow-api-key: YOUR_API_KEY"
```
```python
import requests
pet_id = "pet-uuid-1234"
url = f"https://api.petify.petnow.io/v2/pets/{pet_id}"
headers = {
"x-petnow-api-key": "YOUR_API_KEY"
}
response = requests.delete(url, headers=headers)
print(response.status_code) # 204 No Content
```
```javascript
const petId = "pet-uuid-1234";
const response = await fetch(`https://api.petify.petnow.io/v2/pets/${petId}`, {
method: "DELETE",
headers: {
"x-petnow-api-key": "YOUR_API_KEY"
}
});
console.log(response.status); // 204 No Content
```
### Response [#response-4]
Returns `204 No Content` on success.
**Warning**: Deleting a pet also deletes all registered fingerprints and appearance images. **Deletion is not reversible for the integrator** — there is no restore API, so confirm the `petId` before calling.
Deleting a pet that is already gone returns `404` with `PETNOWB2B20000`, so a repeated call is safe.
### Pets created by Web Integration [#pets-created-by-web-integration]
Pets registered through [Web Integration](/petify/docs/server/web-integration/intro) are referenced by your `externalPetId` only — their Petify `petId` is **not exposed** to you, so they **cannot be deleted through this API**. Delete them in the Petify Console under **Biometric Registration Results** by searching for the `externalPetId` (see [Petify Console](/petify/docs/petify-console)). The Console delete has the same effect as `DELETE /v2/pets/{petId}`: the pet is excluded from matching immediately and the `externalPetId` becomes registrable again.
Keep your own **`externalPetId` ↔ member mapping** on your side. It is the only handle you have for a Web Integration pet, and you will need it to find and delete the record when a member cancels.
### Cleaning up incomplete records [#cleaning-up-incomplete-records]
When a registration flow aborts after `POST /v2/pets` succeeded — for example the upload or `addFingerprints` step failed — the pet profile remains with `hasFingerprint: false`. Such a pet has no biometric data, but it **still counts as a registered pet at the billing snapshot**. Delete it rather than leaving it behind:
1. List pets with `GET /v2/pets`.
2. Filter for `hasFingerprint == false` (skip any profile whose registration is still in progress).
3. `DELETE /v2/pets/{petId}` each one. `204` means deleted; `404 PETNOWB2B20000` means it was already gone — either way the re-run is safe.
```bash
curl -s "https://api.petify.petnow.io/v2/pets" \
-H "x-petnow-api-key: YOUR_API_KEY" \
| jq -r '.data[] | select(.hasFingerprint == false) | .id' \
| while read -r pet_id; do
curl -s -o /dev/null -w "$pet_id %{http_code}\n" -X DELETE \
"https://api.petify.petnow.io/v2/pets/$pet_id" \
-H "x-petnow-api-key: YOUR_API_KEY"
done
```
We recommend calling `DELETE` directly in your error handling whenever a registration flow fails after the profile was created, and auditing for `hasFingerprint: false` profiles periodically (e.g. before each billing date).
### Member cancellation checklist [#member-cancellation-checklist]
When a member leaves your service, remove their pet so it is neither matched nor billed:
1. Look up the pet's `petId` from your own mapping (or `GET /v2/pets` and match on `metadata`).
2. Call `DELETE /v2/pets/{petId}`.
3. Confirm the response is `204` (or `404 PETNOWB2B20000` if it was already deleted).
4. Drop the mapping on your side.
For pets registered through Web Integration, replace steps 1–3 with the Console delete described above.
## Metadata Schema Example [#metadata-schema-example]
Here's an example schema for the `metadata` field:
```json
{
"name": "Buddy",
"age": 3,
"gender": "male",
"owner": {
"name": "John Doe",
"phone": "010-1234-5678",
"email": "user@example.com"
},
"vaccinated": true,
"registrationNumber": "123456789",
"notes": "Has allergies"
}
```
**Tip**: Since metadata is stored as a JSON string, you need to parse it when retrieving. You can freely define field names and structure according to your business requirements.
## Next Steps [#next-steps]
* [Biometric Upload](/petify/docs/server/api/biometric)
* [Registration/Verification/Identification](/petify/docs/server/api/identification)
# Getting Started
Source: /petify/docs/server/web-integration/getting-started
> How to start with Web Integration — from console setup to issuing your first capture link and reading the result.
## Prerequisites [#prerequisites]
### Sign up for the console [#sign-up-for-the-console]
Sign up at [console.petify.petnow.io](https://console.petify.petnow.io/ko). There is no approval step — you can start immediately.
### Register a payment method [#register-a-payment-method]
Register a payment method under My Page → **Payment Info**. It is a prerequisite for creating links and issuing API keys.
Creating your first API Key or your first link (whichever happens first, once per account) automatically grants **50 Integration Credits**. See the [Petify Console](/petify/docs/petify-console) docs for details.
## Choose an integration method [#choose-an-integration-method]
* To start quickly with no development, begin with **Link Integration**.
* If your server needs to receive results in a trusted form and act on them, use **Server Integration**.
See the comparison table in the [Web Integration Introduction](/petify/docs/server/web-integration/intro) for the differences. You can also combine both methods per function within one account.
## Validate the pipeline first with a trial link [#validate-the-pipeline-first-with-a-trial-link]
Whichever method you choose, we recommend a **trial (mock) run as your first step**: create a link with the **Trial (mock)** flag in the Console (or issue a session with `"mock": true`), open it, and pick a bundled sample pet instead of using the camera. Everything after that — quality check, registration or matching, result, redirect, and billing — is the real pipeline, so it validates your return page and result handling end to end without an animal. See [Trial (mock) links](/petify/docs/server/web-integration/link-integration#trial-mock-links) and [Trial (mock) sessions](/petify/docs/server/web-integration/server-integration#trial-mock-sessions).
Pointing the camera at a **photo or a monitor screen** is rejected by the fake-image classifier, so it cannot be used to test the capture flow. Testing with the camera requires a **live animal**; use trial links or mock sessions for pipeline tests.
## Start with Link Integration [#start-with-link-integration]
### Create a link [#create-a-link]
In the Console, go to **Integration → Links → Create link** and enter the name, function (Register/Verify/Identify), target species (Register/Identify), Redirect URL, and registration cap (Register).
### Deliver the link [#deliver-the-link]
Deliver the created link URL to your users. **Register and Verify links require the `externalPetId` query parameter**:
```text
https://capture.petify.petnow.io/l/hl_2Xy7Qa9Kd3Lm4Np5Rs6Tu?externalPetId=YOUR-PET-ID-123
```
### Try a capture [#try-a-capture]
Open the link in a smartphone browser and walk through the capture flow yourself. Capture is mobile-only and does not proceed on desktop.
### Read the result [#read-the-result]
When capture finishes, the browser returns to the Redirect URL with `petify_status`. Completed results are available in the Console, and Verify/Identify links additionally deliver a `petify_code` for a display-only result read.
For the full field descriptions and link management rules, see [Link Integration](/petify/docs/server/web-integration/link-integration).
## Start with Server Integration [#start-with-server-integration]
### Issue an API Key [#issue-an-api-key]
Issue an API Key in the Console under **Integration → API & SDK**. The value is visible only right after creation, so store it safely.
### Register allowed Redirect URLs [#register-allowed-redirect-urls]
Pre-register the URLs you will use under **Integration → Links → Allowed Redirect URLs**. Server issuance has no auto-registration.
### Issue a session [#issue-a-session]
Issue a capture session from your server and deliver the response's `url` to the user (be sure to store the `id`):
```bash
curl -X POST "https://api.petify.petnow.io/v2/hosted-sessions" \
-H "x-petnow-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "REGISTER",
"species": "DOG",
"redirectUrl": "https://your-service.example.com/petify/return",
"externalPetId": "YOUR-PET-ID-123"
}'
```
### Receive the result [#receive-the-result]
Consume the completion redirect's `petify_code` via `GET /v2/hosted-results/{code}` to receive the result. If the redirect is lost, read the session via `GET /v2/hosted-sessions/{id}`.
For the full endpoint specs and error codes, see [Server Integration](/petify/docs/server/web-integration/server-integration).
## Local development tip [#local-development-tip]
Redirect URLs require HTTPS in principle, but loopback hosts (`localhost`, `127.0.0.1`, `::1`) may use HTTP for local development. For example, register `http://localhost:3000/petify/return` on the allow-list to walk through the full flow locally.
## Next steps [#next-steps]
* [Link Integration](/petify/docs/server/web-integration/link-integration) - Issuing and managing links and reading results
* [Server Integration](/petify/docs/server/web-integration/server-integration) - The full session API specs
* [Redirect URLs & Result Delivery](/petify/docs/server/web-integration/redirect-urls) - Allow-list rules and redirect parameters
# Web Integration Introduction
Source: /petify/docs/server/web-integration/intro
> Introduces Web Integration, which provides pet biometric recognition through a Petnow-hosted web capture page — no app install or SDK integration required.
## What is Web Integration? [#what-is-web-integration]
**Web Integration** delivers pet biometric recognition through a **Petnow-hosted mobile web capture page**. All you hand your end user is **a single capture URL**. The user opens the link in their smartphone browser, captures their pet's nose, and when processing finishes the browser automatically returns to the **Redirect URL** you configured.
The capture UI, real-time guidance, quality checks, uploads, and processing state are all handled by the Petnow-hosted web app. The only things you implement are **delivering the link** and **receiving the result**.
**When should you choose Web Integration?**
* You run a web-based service without a mobile app
* You want a quick PoC/pilot before integrating the mobile SDK
* You deliver capture links through SMS, messengers, or QR codes
To embed the capture UI directly in your app, see the [iOS](/petify/docs/sdk/ios/intro) · [Android](/petify/docs/sdk/android/intro) · [React Native](/petify/docs/sdk/react-native/intro) SDKs.
## Supported functions [#supported-functions]
| Function | Action | Description |
| ------------ | ---------- | ----------------------------------------------------- |
| **Register** | `REGISTER` | Registers a new pet's biometric profile |
| **Verify** | `VERIFY` | Checks whether the pet matches a registered pet (1:1) |
| **Identify** | `IDENTIFY` | Finds candidate matches for the captured pet (1:N) |
* Both **dogs (`DOG`) and cats (`CAT`)** are supported. Register and Identify take a target species when the link (session) is created; Verify automatically follows the species of the target pet.
* The Identify (`IDENTIFY`) function may be restricted depending on your plan. See [Pricing](https://www.petnow.io/en/pricing) for details.
## Two integration methods [#two-integration-methods]
Web Integration comes in two methods, depending on **who issues the capture session**. End users get the same capture page either way.
| | Link Integration | Server Integration |
| ---------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| **Session issuance** | Create a link up front in the [Petify Console](/petify/docs/petify-console) | Your server issues a session per user via API |
| **Development needed** | Little to none (deliver a link + a return page) | Server-side integration (API Key required) |
| **Pet identification** | Passed as an `externalPetId` query on the link URL — not vouched for by your server | Set by your server at issuance — a trusted value |
| **Result delivery** | View in the Console + (Verify/Identify) a display-only code | One-time result code consumed by your server + session polling |
| **Best for** | Pilots, small-scale operation, manual result handling | Automated services that act on results |
* **[Link Integration](/petify/docs/server/web-integration/link-integration)** — create a reusable link in the Console and distribute it. You can start with no development.
* **[Server Integration](/petify/docs/server/web-integration/server-integration)** — your server issues a one-time session URL via `POST /v2/hosted-sessions` and receives the completion result in a trusted form.
If you need to make business decisions based on the result (granting points, insurance screening, and so on), use **Server Integration**. Link Integration's result delivery path is display-only.
## The end-user flow [#the-end-user-flow]
1. Opening the capture link creates a **session**. Sessions expire after 30 minutes by default.
2. The user reviews the capture guide and captures the pet's nose with the camera.
3. After a quality check, the submission is processed as a registration, verification, or identification.
4. When processing finishes, the browser automatically navigates to your Redirect URL with the result status attached as query parameters. See [Redirect URLs & Result Delivery](/petify/docs/server/web-integration/redirect-urls) for the full rules.
### Capture environment requirements [#capture-environment-requirements]
* **Mobile only**: capture cannot proceed in desktop browsers; users are asked to reopen the link on a mobile device.
* **Camera permission**: the browser's camera access permission is requested when capture starts.
* The flow works regardless of how the link is opened — new tab, same tab, or in-app webview.
### Testing without an animal [#testing-without-an-animal]
Use a **trial (mock) link** or a session issued with `"mock": true` as your first integration step: the user picks a bundled sample pet instead of using the camera, and everything from the quality check onward — registration or matching, result, redirect, and billing — runs through the real pipeline. See [Getting Started](/petify/docs/server/web-integration/getting-started).
Pointing the camera at a **photo or a monitor screen** is rejected by the fake-image classifier. Camera testing requires a **live animal**; use trial links for pipeline tests.
## Next steps [#next-steps]
To adopt Web Integration, see the following guides:
* [Getting Started](/petify/docs/server/web-integration/getting-started) - From console setup to your first capture and result
* [Link Integration](/petify/docs/server/web-integration/link-integration) - Issue and manage links in the Console
* [Server Integration](/petify/docs/server/web-integration/server-integration) - Issue sessions and receive results via API
* [Redirect URLs & Result Delivery](/petify/docs/server/web-integration/redirect-urls) - The allow-list and redirect parameters
# Link Integration
Source: /petify/docs/server/web-integration/link-integration
> How to issue and manage capture links in the Petify Console, and how to read the results of link-created sessions.
## Overview [#overview]
**Link Integration** means creating a **reusable capture link** in the [Petify Console](/petify/docs/petify-console) and distributing it to your users. You can start without any server development, and a single link can serve many users.
The Console's **Integration** menu provides link issuance and management (the Links tab) together with API credential management (the API & SDK tab).
**Registering a payment method is a prerequisite** for creating links. Without one, the Console does not proceed to link creation. See the [Petify Console](/petify/docs/petify-console) docs for the steps.
## Creating a link [#creating-a-link]
Create links in the Console under **Integration → Links → Create link**.
| Field | Required | Description |
| -------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | ✅ | A name to tell links apart (up to 120 characters) |
| **Function** | ✅ | One of `Register` / `Verify` / `Identify`. **Cannot be changed after creation** |
| **Target species** | Conditional | `Dog` / `Cat`. Set **only on Register and Identify links**, and cannot be changed after creation. A Verify link automatically follows the target pet's species |
| **Redirect URL** | ✅ | The full URL the user returns to after capture. Entering a URL not on the allow-list registers it automatically along with the link |
| **Registration cap** | Conditional | **Register links only.** The maximum number of pets that can be registered through this link (default 100, minimum 1) |
| **Trial (mock)** | ❌ | Creates a [trial (mock) link](#trial-mock-links) whose sessions skip the camera and use a sample pet. **Cannot be changed after creation** |
* To operate with a different target species, create a new link.
* An Identify link searches for candidates only among **pets registered under the same species** as its target species.
* The Identify (`IDENTIFY`) function can only be selected when your plan includes identification.
* For Redirect URL format rules (HTTPS, reserved parameters, and so on), see [Redirect URLs & Result Delivery](/petify/docs/server/web-integration/redirect-urls).
A created link gets a URL of the following form:
```text
https://capture.petify.petnow.io/l/hl_2Xy7Qa9Kd3Lm4Np5Rs6Tu
```
### Trial (mock) links [#trial-mock-links]
A link created with the **Trial (mock)** flag lets you validate the whole pipeline without an animal at hand. Every session it mints **skips the camera** and shows a **bundled sample-pet picker**; the chosen images then go through the real quality check, registration or matching, result, and redirect.
* The flag is **immutable** after creation — a live link cannot be turned into a trial link or vice versa. Create a separate link for each.
* **Billing, caps, results, and the session list are identical** to a normal link. The registered pet, the result, and the Console's session views carry **no mock marker** — the link itself is the only place the flag is visible.
* For a mock **Register** link, the flow mints an `externalPetId` of the form `mock-<12 hex>` when the click URL carries none, so the URL shown in the Console can be opened as-is. If you do pass one, the normal Register rules apply.
* Registrations from a trial link count toward the link's cap and are billable pets — delete them in the Console when you are done (see [Petify Console](/petify/docs/petify-console)). **Never distribute a trial link to real end users.**
## Delivering the link URL [#delivering-the-link-url]
### Passing the pet identifier (`externalPetId`) [#passing-the-pet-identifier-externalpetid]
**Register and Verify links** need to know which pet the capture is for, so append **your own system's pet ID** as the `externalPetId` query parameter when delivering the link:
```text
https://capture.petify.petnow.io/l/hl_2Xy7Qa9Kd3Lm4Np5Rs6Tu?externalPetId=YOUR-PET-ID-123
```
| Function | `externalPetId` |
| -------- | ---------------------------------------------------------------------------------------------------------------- |
| Register | ✅ Required — the pet is registered under this ID. Capture is blocked if the ID is already registered |
| Verify | ✅ Required — compared against the pet registered under this ID. Capture is blocked if the ID has no registration |
| Identify | ❌ Ignored — deliver the link URL as-is |
An `externalPetId` on the link URL **travels through the user's browser**, so it can be tampered with. The Console's result views and server-side reads mark it as **untrusted (`bindingTrusted: false`)**. If you need to make important decisions based on the result, use [Server Integration](/petify/docs/server/web-integration/server-integration).
`externalPetId` bindings are created **only through Web Integration registration**. Pets registered through the mobile SDKs or the [Server API](/petify/docs/server/api/overview) have no binding, so they cannot be targeted for verification — and there is no feature for adding a binding to an existing pet. To use Verify/Identify, the target pet must first go through a Web Integration registration.
### When entry is blocked [#when-entry-is-blocked]
In the following cases the capture page does not open and the user sees a notice screen instead:
* A Register or Verify link is opened without an `externalPetId`
* Register: the `externalPetId` is already registered
* Verify: the `externalPetId` has no registration
When entry is blocked, **no session is created, so no navigation to the Redirect URL happens either.** Your return page is never called and nothing appears in the Console's result list, so waiting for `petify_status=failed` cannot detect it. We recommend checking the ID's duplication/registration state on your side before handing out the link.
## Link states [#link-states]
| State | Meaning |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Active** | Users can open the link and capture |
| **Inactive** | A link you disabled manually. New capture entries are blocked until you re-activate it |
| **Cap reached** | A Register link automatically stopped at its registration cap. **Raising the cap above the current registration count re-activates it automatically** |
| **Plan limited** | An Identify link stopped because your current plan does not include identification. The link and its settings are kept, and it **recovers automatically once you switch to a plan that includes identification** (a manually disabled link does not auto-recover) |
* Links have no expiry date or scheduled shutdown — they stay usable until deactivated, deleted, or their cap is reached.
* Activating/deactivating takes effect immediately from the link detail view.
* Deactivation and deletion block not only new capture entries but also **submissions not yet accepted**. A session whose final submission was already accepted is processed to completion even if the link state changes afterwards.
## Managing links [#managing-links]
* **Editing**: the name, Redirect URL, and registration cap can be edited. The function and target species cannot be changed.
* **Cap lower bound**: the cap cannot be set below the current cumulative registration count. The count is lifetime-cumulative for the link and does not decrease when registered pets are deleted.
* **Deletion**: deletion is permanent and unrecoverable, and the link address is never reused. Pets registered through the link are kept.
* **Count limit**: an account can hold at most **500 links** (excluding deleted ones).
## Reading results [#reading-results]
When capture finishes, the user's browser navigates to the link's Redirect URL with `petify_status` (and conditionally `petify_code`) added as query parameters. See [Redirect URLs & Result Delivery](/petify/docs/server/web-integration/redirect-urls) for the full parameter rules.
### Identifying the returning user [#identifying-the-returning-user]
The Redirect URL is a fixed value on the link (exact-match against the allow-list), and no user or session identifier is attached to the redirect. Determine "who came back" on your return page as follows:
* The returning browser is **the very browser that opened the link.** If the link was opened from a CTA on your service's page, your login session (cookies) is still there, so **identifying the user by your own session** on the return page is the primary method. If you delivered the link through a channel with no login session (SMS, QR), require a sign-in on the return page or look the result up by `externalPetId` in the Console.
* Which pet the result belongs to is defined by the `externalPetId` you attached when handing out the link, and the Console's result views show that ID too.
* Dynamically appending per-user parameters to the Redirect URL is not supported. If you need reliable per-session correlation, use [Server Integration](/petify/docs/server/web-integration/server-integration) — the session `id` you stored at issuance matches the result's `sessionId`/`externalPetId`.
### Register links: the redirect is the result [#register-links-the-redirect-is-the-result]
A Register link's completion redirect carries **no `petify_code`**. `petify_status=completed` itself means the registration succeeded — there is nothing extra to fetch. Details of the registered pet are available in the Console.
### Verify and Identify links: a display-only code [#verify-and-identify-links-a-display-only-code]
A Verify or Identify link's completion redirect carries a one-time **display code** (`petify_code=hrc_...`). Your return page's frontend can read it without credentials to show the user their result:
```javascript
// Your return page (frontend)
const params = new URLSearchParams(window.location.search);
if (params.get("petify_status") === "completed") {
const code = params.get("petify_code");
const response = await fetch(
`https://api.petify.petnow.io/hosted/v1/results/${code}:display`
);
const { data } = await response.json();
// Verify: { action: "VERIFY", outcome: "MATCH", isVerified: true, completedAt: "..." }
// Identify: { action: "IDENTIFY", outcome: "MATCH", isVerified: null, completedAt: "..." }
}
```
* The code stays valid for about 10 minutes after the session completes, and can be re-read within that window — a page refresh on your result screen keeps working.
* The response carries only a coarse result (`outcome`, plus `isVerified` for Verify). **Scores, candidate lists, and pet IDs are never exposed.**
The display-code read is for **on-screen display only**. It is reachable without credentials, so never use it as the basis for business or security decisions. If you need a trusted result, use [Server Integration](/petify/docs/server/web-integration/server-integration).
### In the Console [#in-the-console]
The Console is the only place that shows a completed session's **full result** (including the Petify Pet ID and Identify candidates with scores). Each Register link's progress (cumulative registrations / cap) is also visible on the link detail view.
### Server-side reads (optional) [#server-side-reads-optional]
If you have an API key, sessions created from links can also be read server-side via `GET /v2/hosted-sessions/{id}`. Session IDs are shown in the Console's session views. See the session polling section of [Server Integration](/petify/docs/server/web-integration/server-integration) for details.
## Next steps [#next-steps]
* [Server Integration](/petify/docs/server/web-integration/server-integration) - Issue sessions from your server and receive trusted results
* [Redirect URLs & Result Delivery](/petify/docs/server/web-integration/redirect-urls) - Allow-list management and redirect parameters
# Redirect URLs & Result Delivery
Source: /petify/docs/server/web-integration/redirect-urls
> The allowed Redirect URL rules, and the parameters delivered on the post-capture redirect.
## What is the Redirect URL allow-list? [#what-is-the-redirect-url-allow-list]
The **Redirect URL a user returns to after capture must be registered on an allow-list**. This is a safeguard against sending users to arbitrary addresses (open redirects).
The allow-list is managed in the Console under **Integration → Links → Allowed Redirect URLs**, and is shared by [Link Integration](/petify/docs/server/web-integration/link-integration) link settings and [Server Integration](/petify/docs/server/web-integration/server-integration) session issuance.
| How to register | Description |
| ------------------------------------------- | ------------------------------------------------------------------------------ |
| **Add directly in the Console** | Add the URL on the Allowed Redirect URLs page |
| **Auto-registration on link create/update** | Entering an unregistered URL on a link adds it to the allow-list automatically |
**Server Integration does not auto-register.** `POST /v2/hosted-sessions` only accepts pre-registered URLs; an unregistered URL returns a 400 error (`PETNOWB2B21003`).
## URL registration rules [#url-registration-rules]
| Rule | Description |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **HTTPS required** | Only `https://` URLs can be registered. As an exception, loopback development hosts (`localhost`, `127.0.0.1`, `::1`) may use `http://` (any port, path, or query) |
| **Exact match** | A link's or session's Redirect URL must match a registered entry **character for character** (loopback URLs must be registered exactly too, port included) |
| **Reserved parameters forbidden** | The query string must not contain the keys `petify_status` or `petify_code` — these are the reserved parameters Petnow appends on the completion redirect |
| **Length limit** | URLs are limited to 500 characters |
## URL deletion rules [#url-deletion-rules]
* You cannot delete a URL while it is used by a link that has not been deleted. First change the connected link's Redirect URL or delete the link.
* A URL previously used by a deleted link may still be protected from deletion even when the number of connected links is shown as `0`. In that case, leave the existing URL in the allow-list and register a new URL separately. An allow-list entry that is not used by any link does not affect capture or redirect behavior.
* In-progress capture sessions use the Redirect URL captured at creation time, so allow-list changes do not affect already-issued sessions.
## Completion redirect parameters [#completion-redirect-parameters]
When a capture session ends, the user's browser navigates to the Redirect URL, with query parameters appended by Petnow. Your existing query parameters and fragment are preserved — only the result keys are **appended** — and no other parameters (session IDs, scores, tokens, ...) are ever added.
The redirect happens **only for sessions that were actually created**. When entry itself is blocked (a duplicate or unregistered `externalPetId`, an inactive link, and so on), there is no session and your return page is never called.
```text
https://your-service.example.com/petify/return?petify_status=completed&petify_code=hrc_1a2b3c4d5e6f
```
### `petify_status` [#petify_status]
| Value | Meaning |
| ----------- | ------------------------------------------------------------------------------------- |
| `completed` | Finished normally |
| `cancelled` | The user cancelled the capture |
| `failed` | Processing failed (e.g. insufficient capture quality), or the feature was unavailable |
| `expired` | The session expired without completing |
A Verify **mismatch** and an Identify **no-candidate** outcome are `completed`, not failures — the function result was produced normally, and you learn the match/candidate outcome by reading the result. `failed` means no result could be produced at all.
### `petify_code` [#petify_code]
`petify_code` means different things depending on the status:
| `petify_status` | `petify_code` |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `completed` | A one-time result/display code (`hrc_...`) — see the issuance matrix below |
| `failed` | A machine-readable reason. Currently only `FEATURE_UNAVAILABLE` — the feature is unavailable on the account due to its plan/contract state; check your plan in the Console. Other failures (e.g. insufficient capture quality) carry no code |
| `cancelled`, `expired` | Never present |
**When a code is issued on `completed`**:
| Session kind | `petify_code` |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| Link Integration — Register | **Absent** — `petify_status=completed` itself is the registration outcome; details are in the Console |
| Link Integration — Verify / Identify | Present — a display-only code (see [Link Integration](/petify/docs/server/web-integration/link-integration)) |
| Server Integration — every action | Present — a trusted result code (see [Server Integration](/petify/docs/server/web-integration/server-integration)) |
**`petify_status=failed&petify_code=FEATURE_UNAVAILABLE`** is issued when an Identify session lost its identification entitlement between issuance and final submit (the plan does not include identification, or a plan has not been selected yet). It is deliberately a **single neutral value for both causes**, so the end user's URL never reveals plan or contract detail. No result is produced and nothing is billed for such a session; on the server side it reads as `FAILED` with `errorCode` `IDENTIFICATION_NOT_IN_PLAN` / `IDENTIFICATION_PLAN_SELECTION_REQUIRED` (see [Server Integration](/petify/docs/server/web-integration/server-integration)). This gating may start being enforced in a future release.
Your return page does not need to branch on the session kind — **just read whatever parameters you are given**. The `petify_` prefix exists so the keys cannot collide with query parameters your page already uses, such as `status` or `code`.
## Return-page implementation tips [#return-page-implementation-tips]
* No user or session identifier is attached to the redirect, and dynamically appending per-user parameters to the Redirect URL is not supported (exact-match allow-list). With Link Integration, identify the returning user by **your own login session** in the returning browser; with Server Integration, match the result's `sessionId`/`externalPetId` against what you stored at issuance.
* Handle the non-`completed` statuses too (cancelled, failed, expired). For failed or expired sessions, we recommend offering the user a retry path (a fresh link or session).
* Result codes stay valid for about 10 minutes after the session completes. The redirect always arrives well within that window, but avoid designs that defer code handling to a batch job.
* With Server Integration, design for the redirect possibly being lost — even if the user closes the browser mid-navigation, you can recover the result by polling `GET /v2/hosted-sessions/{id}`.
## Next steps [#next-steps]
* [Link Integration](/petify/docs/server/web-integration/link-integration) - Issue and manage links in the Console
* [Server Integration](/petify/docs/server/web-integration/server-integration) - Issue sessions and receive results via API
# Server Integration
Source: /petify/docs/server/web-integration/server-integration
> The Server Integration API for issuing capture sessions from your server and receiving trusted results.
## Overview [#overview]
**Server Integration** means your server issues a **one-time capture session** per user via API and receives the completion result **in a trusted, server-to-server form**. It uses the same host and authentication (`x-petnow-api-key`) as the [Server API](/petify/docs/server/api/overview).
| Item | Value |
| --------------- | ------------------------------------------------------------------------------------------------- |
| Base URL | `https://api.petify.petnow.io` |
| Authentication | `x-petnow-api-key` header ([issue an API Key](/petify/docs/petify-console)) |
| Response format | Success: `{ "success": true, "data": ... }` / Error: `{ "errors": [{ "code": "PETNOWB2B..." }] }` |
### The full round-trip [#the-full-round-trip]
1. Your server issues a session via `POST /v2/hosted-sessions` and delivers the **one-time web URL** from the response to the end user.
2. When the user finishes capturing on the Petnow-hosted page, the browser returns to your `redirectUrl`. On completion, `petify_code` carries a one-time result code.
3. Your server trades the code for the trusted result via `GET /v2/hosted-results/{code}` (**one-shot consume**).
4. If the redirect or the code is lost, you can always read the same payload via `GET /v2/hosted-sessions/{id}`.
### The pet identity model [#the-pet-identity-model]
On this API, pets are referenced **exclusively by your own system's ID (`externalPetId`)**. Internal Petify IDs are never exposed. Registration (`REGISTER`) binds the `externalPetId` to the pet, and later Verify/Identify results come back under that ID.
Bindings are created **only through Web Integration registration**. Pets registered through the mobile SDKs or the [Server API](/petify/docs/server/api/overview) (`/v2/pets`) have no binding, so they cannot be targeted for verification (404 `PETNOWB2B21002`), and in identification they are never returned as candidate IDs — only counted in `unmappedCandidates`. There is no feature for adding a binding to an existing pet.
## Prerequisites [#prerequisites]
1. Issue an **API Key** in the [Petify Console](/petify/docs/petify-console). (It can also be managed from the Console's **Integration → API & SDK** tab.)
2. **Pre-register** the Redirect URLs you will use under the Console's **Integration → Links → Allowed Redirect URLs**.
Server Integration only accepts Redirect URLs that are **already on the allow-list** (exact match). Issuing a session with an unregistered URL returns a 400 error (`PETNOWB2B21003`). See [Redirect URLs & Result Delivery](/petify/docs/server/web-integration/redirect-urls) for the registration rules.
## Issuing a session [#issuing-a-session]
**Endpoint**: `POST /v2/hosted-sessions`
### Request [#request]
```bash
curl -X POST "https://api.petify.petnow.io/v2/hosted-sessions" \
-H "x-petnow-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "REGISTER",
"species": "DOG",
"redirectUrl": "https://your-service.example.com/petify/return",
"externalPetId": "YOUR-PET-ID-123",
"expiresInSeconds": 1800,
"locale": "en"
}'
```
```python
import requests
response = requests.post(
"https://api.petify.petnow.io/v2/hosted-sessions",
headers={
"x-petnow-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"action": "REGISTER",
"species": "DOG",
"redirectUrl": "https://your-service.example.com/petify/return",
"externalPetId": "YOUR-PET-ID-123",
"expiresInSeconds": 1800,
"locale": "en",
},
)
session = response.json()["data"]
# session["id"] -> polling handle (store it!)
# session["url"] -> the one-time capture URL to hand to the user
```
```javascript
const response = await fetch("https://api.petify.petnow.io/v2/hosted-sessions", {
method: "POST",
headers: {
"x-petnow-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
action: "REGISTER",
species: "DOG",
redirectUrl: "https://your-service.example.com/petify/return",
externalPetId: "YOUR-PET-ID-123",
expiresInSeconds: 1800,
locale: "en"
})
});
const { data: session } = await response.json();
// session.id -> polling handle (store it!)
// session.url -> the one-time capture URL to hand to the user
```
### Request parameters [#request-parameters]
| Field | Type | Required | Description |
| ------------------ | ------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action` | string | ✅ | `REGISTER` / `VERIFY` / `IDENTIFY` |
| `species` | string | ✅ | `DOG` or `CAT`. For `VERIFY` it must match the target pet's species; `IDENTIFY` searches for candidates only among pets registered under this species |
| `redirectUrl` | string | ✅ | The URL to return to on completion. Must **exactly match** an allow-list entry |
| `externalPetId` | string | Conditional | Your system's pet ID (up to 255 characters). Required for `REGISTER`/`VERIFY`; must not be sent for `IDENTIFY` |
| `petMetadata` | string | ❌ | `REGISTER` only. A free-form string stored on the registered pet's `metadata` field |
| `expiresInSeconds` | integer | ❌ | Session expiry (300–86400 seconds, default 1800) |
| `locale` | string | ❌ | Language hint for the capture page (`ko` default, `en` supported) |
| `mock` | boolean | ❌ | Issues a **trial (mock) session** (default `false`). The user picks a bundled sample pet instead of using the camera; everything after that is the real pipeline. Echoed in the response — see [Trial (mock) sessions](#trial-mock-sessions) |
### Response (201) [#response-201]
```json
{
"success": true,
"data": {
"id": "8f6a1f9e-4c2d-4b7a-9e1a-2f3b4c5d6e7f",
"url": "https://capture.petify.petnow.io/s/hs_QzR4c2VjcmV0dG9rZW4",
"action": "REGISTER",
"species": "DOG",
"integrationMethod": "SERVER_INTEGRATION",
"bindingSource": "SERVER_ISSUED",
"externalPetId": "YOUR-PET-ID-123",
"expiresAt": "2026-08-26T05:30:00Z",
"createdAt": "2026-08-26T05:00:00Z",
"mock": false
}
}
```
* **Store the `id`** together with your own user/order at issuance time — matching it against the result's `sessionId`/`externalPetId` tells you whose result it is, and it is also your only way to read the result if the redirect is lost.
* The `url` is **shown once in the issuance response** and can never be retrieved again. **Deliver each `url` to exactly one end user, once** — avoid designs that re-send the same URL over multiple channels. If a delivery fails, issue a new session rather than re-sending.
### Trial (mock) sessions [#trial-mock-sessions]
Set `"mock": true` to issue a **trial session** for evaluating or wiring up the integration without an animal at hand. The end user who opens the `url` picks a **bundled sample pet** instead of using the camera; everything after that — quality check, registration or matching, the result you consume or poll, the redirect, billing, and caps — is the **real pipeline**.
```bash
curl -X POST "https://api.petify.petnow.io/v2/hosted-sessions" \
-H "x-petnow-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "REGISTER",
"species": "DOG",
"redirectUrl": "https://your-service.example.com/petify/return",
"externalPetId": "TEST-PET-001",
"mock": true
}'
```
* The registered pet and its results carry **no mock marker anywhere** — only this request/response pair does. A mock registration is a real, billable registration; delete test pets in the Console afterwards (see [Petify Console](/petify/docs/petify-console)).
* `externalPetId` rules are unchanged: your server still mints its own IDs.
* **Never hand a mock `url` to a real end user.**
## Receiving the result [#receiving-the-result]
When capture finishes, the user's browser navigates to your `redirectUrl` with `petify_status` (and conditionally `petify_code`) added as query parameters. See [Redirect URLs & Result Delivery](/petify/docs/server/web-integration/redirect-urls) for the full parameter rules.
### Consuming the result code [#consuming-the-result-code]
Trade the one-time result code received with `petify_status=completed` on your server.
**Endpoint**: `GET /v2/hosted-results/{code}`
```bash
curl -X GET "https://api.petify.petnow.io/v2/hosted-results/hrc_1a2b3c4d5e6f" \
-H "x-petnow-api-key: YOUR_API_KEY"
```
```python
import requests
response = requests.get(
"https://api.petify.petnow.io/v2/hosted-results/hrc_1a2b3c4d5e6f",
headers={"x-petnow-api-key": "YOUR_API_KEY"},
)
result = response.json()["data"]
```
```javascript
const response = await fetch(
"https://api.petify.petnow.io/v2/hosted-results/hrc_1a2b3c4d5e6f",
{ headers: { "x-petnow-api-key": "YOUR_API_KEY" } }
);
const { data: result } = await response.json();
```
**Example response (Verify)**:
```json
{
"success": true,
"data": {
"sessionId": "8f6a1f9e-4c2d-4b7a-9e1a-2f3b4c5d6e7f",
"action": "VERIFY",
"externalPetId": "YOUR-PET-ID-123",
"bindingTrusted": true,
"outcome": "MATCH",
"completedAt": "2026-08-26T05:10:00Z",
"alreadyConsumed": false,
"result": {
"isVerified": true,
"score": 93
}
}
}
```
**The `result` schema per action**:
| Action | Fields | Description |
| ---------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `REGISTER` | `registered` (boolean) | Whether the registration succeeded |
| `VERIFY` | `isVerified` (boolean), `score` (0–100) | Match result and score |
| `IDENTIFY` | `candidates` (array), `unmappedCandidates` (integer) | Candidate list `[{ externalPetId, score }]`, score descending. Candidates without an `externalPetId` binding are omitted from the list and counted in `unmappedCandidates` |
**Consumption rules**:
* The result-code read is a **one-shot consume**. Codes stay valid for about 10 minutes after the session completes.
* If the consume succeeded but you lost the response (timeout, crash), re-reading **with the same API key within 5 minutes (default)** returns the identical payload with `alreadyConsumed: true`. After that grace window it returns 410.
* If you lost the code itself, fall back to session polling below.
### Polling the session state [#polling-the-session-state]
The recovery path when the redirect or code is lost, and the way to check a session's progress. It is non-consuming, so you can call it repeatedly.
**Endpoint**: `GET /v2/hosted-sessions/{id}`
```json
{
"success": true,
"data": {
"id": "8f6a1f9e-4c2d-4b7a-9e1a-2f3b4c5d6e7f",
"linkId": null,
"action": "VERIFY",
"species": "DOG",
"integrationMethod": "SERVER_INTEGRATION",
"bindingSource": "SERVER_ISSUED",
"externalPetId": "YOUR-PET-ID-123",
"bindingTrusted": true,
"status": "COMPLETED",
"outcome": "MATCH",
"errorCode": null,
"result": { "isVerified": true, "score": 93 },
"expiresAt": "2026-08-26T05:30:00Z",
"completedAt": "2026-08-26T05:10:00Z",
"createdAt": "2026-08-26T05:00:00Z"
}
}
```
**Session states (`status`)**:
| State | Description |
| ------------ | -------------------------------------------------------- |
| `CREATED` | The session was issued and not yet opened |
| `OPENED` | The user opened the capture page |
| `PROCESSING` | Submitted and being processed |
| `COMPLETED` | Finished normally — `outcome` and `result` are populated |
| `FAILED` | Failed — `errorCode` carries the reason (see below) |
| `EXPIRED` | Expired without completing |
| `CANCELLED` | The user cancelled the capture |
* `outcome` is populated only for `COMPLETED`: `SUCCEEDED` for Register, `MATCH` / `NO_MATCH` for Verify and Identify.
* `errorCode` on `FAILED` is a plain string, not a closed enum — treat the list as open. Current values:
| `errorCode` | Meaning |
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CAPTURE_FAILED` | No result could be produced from the capture (e.g. insufficient quality) |
| `DUPLICATE_EXTERNAL_PET_ID` | Open-mode duplicate `externalPetId` detected at completion |
| `IDENTIFICATION_NOT_IN_PLAN` / `IDENTIFICATION_PLAN_SELECTION_REQUIRED` | An `IDENTIFY` session lost the identification entitlement **between issuance and final submit** (e.g. a plan change while the user was capturing). The submit is refused, no result is produced, and **nothing is billed**. The browser redirect carries `petify_code=FEATURE_UNAVAILABLE`. These codes may start being returned in a future release; handle them now so the change is a non-event |
* This endpoint also serves sessions created from [Link Integration](/petify/docs/server/web-integration/link-integration) links (link sessions carry a `linkId`, and a user-supplied `externalPetId` is marked `bindingTrusted: false`).
## Error codes [#error-codes]
Error responses use the `{ "errors": [{ "code": "..." }] }` shape. `PETNOWB2B10001` (request validation failure) additionally carries per-field details in `details`.
| Code | HTTP | Meaning |
| ---------------- | ---- | -------------------------------------------------------------------------------------------- |
| `PETNOWB2B10000` | 401 | Missing or invalid API Key |
| `PETNOWB2B10001` | 400 | Request validation failed (`details` carries per-field reasons) |
| `PETNOWB2B10005` | 402 | Registered-pet limit reached |
| `PETNOWB2B10010` | 403 | Account suspended for billing reasons |
| `PETNOWB2B10011` | 402 | Identification (`IDENTIFY`) is not included in the current plan |
| `PETNOWB2B10012` | 402 | A plan must be selected (choose a plan in the Console first) |
| `PETNOWB2B20015` | 400 | `species` does not match the `VERIFY` target pet |
| `PETNOWB2B21001` | 409 | `REGISTER`: the `externalPetId` is already registered |
| `PETNOWB2B21002` | 404 | `VERIFY`: the `externalPetId` has no registration |
| `PETNOWB2B21003` | 400 | The `redirectUrl` is not on the allow-list |
| `PETNOWB2B21004` | 404 | Session not found |
| `PETNOWB2B21005` | 404 | Result code unknown or expired (Link Integration display codes are also a 404 on this route) |
| `PETNOWB2B21006` | 410 | Result code already consumed (grace window elapsed) |
## Next steps [#next-steps]
* [Redirect URLs & Result Delivery](/petify/docs/server/web-integration/redirect-urls) - Allow-list management and redirect parameters
* [Link Integration](/petify/docs/server/web-integration/link-integration) - Issue and manage links in the Console