# FAQ Source: /petify/docs/faq > Short answers to the questions we hear most often from integrators. ## Accounts [#accounts] ### Can I log in to the Petify mobile app with my Petify SaaS (Console) account? [#can-i-log-in-to-the-petify-mobile-app-with-my-petify-saas-console-account] Yes. Use the same email and password you use for the [Petify Console](/petify/docs/petify-console). Do not sign up again in the app — signing up with an already-registered email fails at the verification step. ## Testing [#testing] ### Do I need a separate sandbox for integration tests? [#do-i-need-a-separate-sandbox-for-integration-tests] There is no separate sandbox tenant: on a given backend, testing and live traffic share one account and one balance. What you do have is a **staging backend** with its own console and its own keys — prefer it while you are evaluating, because a scan against production is a real registration (see the next answer). On whichever backend you use: spend the **50 Integration Credits** granted when you create your first API key or Web Integration link, delete test pets after each test, and use [Web Integration trial (mock) links](/petify/docs/server/web-integration/link-integration) to test without an animal. ### Which API key and backend should the sample app — or an evaluation build — use? [#which-api-key-and-backend-should-the-sample-app--or-an-evaluation-build--use] Keys are per-backend: a key issued in the staging console works only against staging, and a production key only against production. A mismatch fails license validation with `License validation failed: the API key was rejected by the server.` The SDK picks the backend from `LicenseInfo.environment` (React Native: `usePetnowCamera({ apiKey, environment })`); omit it and you get production. Since a scan against production is a real registration, prefer staging while you are evaluating. ### Why does the camera detect the nose but never start capturing? [#why-does-the-camera-detect-the-nose-but-never-start-capturing] Usually one of two things: the camera is pointed at a photo, a printed image, or a screen, which the fake-image classifier rejects; or the SDK/capture session is misconfigured. See [Troubleshooting](/petify/docs/sdk/troubleshooting) for the details. ### Can I run the Android SDK on an emulator? [#can-i-run-the-android-sdk-on-an-emulator] Yes, from 1.5.0. The AAR includes the `x86_64` ABI, so the SDK runs on the Android emulator. GPU acceleration on an emulator depends on its GL/OpenCL support and falls back to CPU. ## API Keys [#api-keys] ### Which key goes into the SDK, and is it safe to ship it? [#which-key-goes-into-the-sdk-and-is-it-safe-to-ship-it] The SDK takes the same Petnow API key type that the Server API uses, and anything shipped inside an app binary can be read back out of it. Issue a dedicated key for the app and keep your server key out of the binary — see [API Key Security](/petify/docs/sdk/android/getting-started#api-key-security). ## Data [#data] ### Does the SDK upload images? [#does-the-sdk-upload-images] No. The SDK returns local image files to your app; your server uploads them through the [Server API](/petify/docs/server/api/overview). ## Versions [#versions] ### Where is the changelog, and which versions are supported? [#where-is-the-changelog-and-which-versions-are-supported] See the [changelog](/petify/docs/sdk/changelog) for the latest release and the per-platform version history. # Petnow SDK Documentation Source: /petify/docs > Welcome to Petnow SDK Documentation - Pet Biometric Recognition Solution ## Getting Started [#getting-started] Petnow SDK allows you to easily integrate pet verification/identification features into your app using unique biometric information of dogs and cats. ## Add capture to your app (SDK) [#add-capture-to-your-app-sdk] ## Server & Web [#server--web] # Petnow SDK Overview Source: /petify/docs/overview > Introduction to Petnow SDK structure and core features. Petnow SDK Usage scenario ## What is Petnow SDK? [#what-is-petnow-sdk] Petnow SDK is a framework that enables easy integration of **pet biometric verification/identification features** into mobile apps. ### Core Features [#core-features] * **Dog/Cat Unique Biometric Extraction**: High-performance on-device AI for biometric data extraction * **Biometric Verification/Identification**: Pet authentication using biometric information * **Real-time Guidance**: Visual feedback for optimal capture ### Who needs this? [#who-needs-this] * Developers who want to add pet registration/authentication features to their app * Teams who want to improve pet verification/identification accuracy in existing apps * Businesses providing pet biometric verification/identification as a B2B service ## SDK Structure [#sdk-structure] The Petnow **mobile SDK** provides the on-device capture UI. Server communication (image upload, registration, verification/identification) is performed by **your app server** calling the Petnow Server REST API. ### Module Description [#module-description] #### UI Module [#ui-module] Handles camera UI and biometric data extraction — the mobile module you integrate into your app. * **Features**: Real-time detection, progress display, overlay guide, returns captured image files * **Dependencies**: Camera Module (internal) #### Camera Module [#camera-module] Biometric extraction engine (internal module, not directly accessible) * **Features**: ML inference, image processing, quality check #### Server communication [#server-communication] Image upload, pet registration/management, and verification/identification are handled not by the mobile SDK but by **your app server** calling the [Petnow Server REST API](/petify/docs/server/api/overview). Keep the key your server calls with (`x-petnow-api-key`) on the server. The SDK needs a key of its own on the device, so issue a separate one for the app instead of shipping the server key — see the API Key Security section of your platform's guide. ## Typical Usage Flow [#typical-usage-flow] ## How Capture Quality Is Decided [#how-capture-quality-is-decided] The camera SDK decides on the device whether a frame is good enough to keep. When a frame does not meet the bar, the SDK reports the reason through the detection status, so your app can tell the user what to change. **The decision is made on the device: once the camera is open, capture does not wait on the network.** What does need the network: | Step | Network | Notes | | ----------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `initializeCamera()` — license validation | Yes, once per API key per process | The SDK calls the server to validate the key. A rejected key **or an unreachable server** fails with `InvalidLicense` and the camera is not opened. A key validated earlier in the same process is not re-validated, so later sessions in that process do not repeat the validation round trip. The validation result is not persisted across app restarts. | | Capture (`startDetection()` → result) | Not waited on | Detection, quality checks and the returned image files are all local. The SDK reports usage metrics in the background; that never blocks a capture or makes one fail. | | Upload / register / verify / identify | Yes, from your app server | See [Server API](/petify/docs/server/api/overview). Captured files can be kept on the device and uploaded later. | For field work in places without coverage: validate the license while connected (open the camera once after app launch), keep the app process alive, and upload the captured files when connectivity returns. The next app launch needs the network again for the first `initializeCamera()`. ## Next Steps [#next-steps] First, issue your **API key and SDK distribution credentials** from the [Petify Console](/petify/docs/petify-console) (usable immediately after sign-up). Then start integrating with the platform-specific guides: * [\[Petify Console\]](/petify/docs/petify-console) - Issue credentials & manage usage (start here) * [\[iOS SDK\]](/petify/docs/sdk/ios/intro) - iOS app integration guide * [\[Android SDK\]](/petify/docs/sdk/android/intro) - Android app integration guide * [\[React Native SDK\]](/petify/docs/sdk/react-native/intro) - React Native app integration guide * [\[Web Integration\]](/petify/docs/server/web-integration/intro) - Provide the capture flow through a web link, without app integration # Petify Console Source: /petify/docs/petify-console > The Petify admin console for issuing API keys and SDK distribution credentials and managing usage. ## What is Petify Console? [#what-is-petify-console] **Petify Console** (Petify Admin Console) is the web admin console for partners adopting Petify SaaS. It is the starting point for everything you need to integrate the SDK and Server API: it **issues the credentials** required for integration and lets you **manage usage and billing**. | What you do in the console | Description | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Issue an API Key** | The Petnow API key used for the SDK license (`LicenseInfo`) and for [Server API](/petify/docs/server/api/overview) calls | | **Issue AWS CodeArtifact credentials** | The CodeArtifact credentials needed to download the iOS (SPM) / Android (Maven) / React Native (npm) SDK | | **Register a payment method / manage billing** | Register a payment method for usage-based billing and manage invoices | | **Usage dashboard** | View registration / verification / identification usage and the estimated charge | | **Web Integration link management** | Create and manage [Web Integration](/petify/docs/server/web-integration/intro) capture links and the allowed Redirect URLs | | **Biometric Registration Results** | Search pets registered through Web Integration by `externalPetId` and delete them (their Petify `petId` is not exposed, so the [Server API](/petify/docs/server/api/pet-management) cannot delete them) | * **Console URL**: [console.petify.petnow.io](https://console.petify.petnow.io/ko) * **Product / pricing**: [petnow.io/petify](https://www.petnow.io/ko/petify) · [Pricing](https://www.petnow.io/ko/pricing) Every credential marked "provided by Petify Console" in the install guides (the AWS credentials and the API key) is issued here. *** ## 1. Create an account and sign in [#1-create-an-account-and-sign-in] 1. Go to [console.petify.petnow.io](https://console.petify.petnow.io/ko). 2. Sign up. **You can start using it immediately after sign-up — there is no approval step.** 3. Once signed in, you have access to the **Dashboard** and **My Page**. Sign-up is self-service with no approval wait. For further questions, contact [support@petnow.io](mailto:support@petnow.io). *** ## 2. Register a payment method (prerequisite for issuing keys) [#2-register-a-payment-method-prerequisite-for-issuing-keys] **Issuing a new API Key and AWS CodeArtifact credentials requires a registered payment method.** Without one, the console does not proceed to the key-issuance step. Register a payment method under **Payment Info** in My Page. After registering, creating your first API Key or your first [Web Integration](/petify/docs/server/web-integration/intro) link (whichever happens first, once per account) automatically grants **50 Integration Credits**. * Integration Credits are applied first to your billable-pet usage (1 Credit = 1 billable pet). * They expire about 60 days after they are granted; usage after expiry/depletion is charged to your registered payment method. * Billing is usage-based — a monthly snapshot of registered pets. See [Plans and billing](#6-plans-and-billing) below and [Pricing](https://www.petnow.io/ko/pricing). *** ## 3. Issue an API Key [#3-issue-an-api-key] The API Key is the **Petnow API key** used for the SDK license and Server API authentication. 1. Go to **API Key Management** in My Page. 2. Click **Create new key**. (If no payment method is registered, a registration prompt appears — see step 3.) 3. Enter a key name and create it; the API Key is shown. **The full value is visible only right after creation, so store it somewhere safe.** Use the issued API Key for: * **SDK license** — passed as `LicenseInfo` when creating the camera controller * **Server API** — passed as the `x-petnow-api-key` header on registration / verification / identification calls Never hardcode the API Key in a client app or source code. Inject it via environment variables / a secrets manager, and keep the server-call key on your app server. * **Multiple keys can be active at once**, so you can issue a new key and switch over before retiring the old one. * **A deactivated key cannot be reactivated.** If you deactivate a key by mistake, create a new one. * For the step-by-step rotation guide, see [API key security](/petify/docs/sdk/android/getting-started#api-key-security) on the SDK Getting Started pages. *** ## 4. Issue AWS CodeArtifact credentials [#4-issue-aws-codeartifact-credentials] The Petnow SDK is distributed through AWS CodeArtifact. To download the SDK you must issue CodeArtifact credentials from the console. 1. Issue the **AWS CodeArtifact Key** (credentials) in My Page. (Like the API Key, this requires a registered payment method.) 2. Set the install-guide environment variables with the issued values: ```bash export AWS_ACCESS_KEY_ID= export AWS_SECRET_ACCESS_KEY= export AWS_DEFAULT_REGION= export PETNOW_CODEARTIFACT_DOMAIN= export PETNOW_AWS_ACCOUNT_ID= # Platform-specific repository export PETNOW_IOS_SDK_REPOSITORY= # iOS export PETNOW_ANDROID_SDK_REPOSITORY= # Android export PETNOW_NPM_REPOSITORY= # React Native ``` For the full per-platform install steps, see each getting-started guide: * [iOS Getting Started](/petify/docs/sdk/ios/getting-started) (Swift Package Manager) * [Android Getting Started](/petify/docs/sdk/android/getting-started) (Gradle/Maven) * [React Native Getting Started](/petify/docs/sdk/react-native/getting-started) (npm) *** ## 5. Check usage [#5-check-usage] In **Service Usage** on My Page you can see your current usage, your Integration Credits balance (remaining Credits / to be applied / expiry date), and the estimated charge. The actual applied Credits and charge are finalized based on usage at the billing date. Values shown in the console are estimates based on current registrations. There is **no automatic spending limit, billing cap, or usage alert**. Monitor your registered-pet count under My Page → Service Usage, and delete pets you no longer need before the billing date. *** ## 6. Plans and billing [#6-plans-and-billing] ### Plans [#plans] Two self-service plans are available, priced **per registered pet per month**: | Plan | Price | Includes | | --------------- | --------------------------------- | --------------------------------- | | **Petify Core** | USD 0.50 / registered pet / month | Registration + verification (1:1) | | **Petify Plus** | USD 0.75 / registered pet / month | Core + identification (1:N) | Existing accounts on the legacy plan receive a **30-day notice** with a migration date. If that date passes with no plan chosen, identification requests may start being refused with `PETNOWB2B10012` until a plan is selected under My Page → Billing; selecting either plan clears it. Identification will also require Plus, and a plan without it answers `PETNOWB2B10011`. Registration and verification are unaffected on every plan. Handle both codes now so the change is a non-event. See [Error Codes](/petify/docs/server/api/error-codes). ### Billing rule: a snapshot on the billing date [#billing-rule-a-snapshot-on-the-billing-date] Billing is a **snapshot of the billable pet profiles registered at the billing date**, not a running tally: * A pet deleted **before** the snapshot is not counted and consumes no Integration Credit. A profile with no fingerprints yet still counts, so clean up incomplete registrations (see [Pet Management](/petify/docs/server/api/pet-management)). * Charges already made are **not recalculated** when a pet is deleted afterwards. * **Integration Credits** are applied on each billing date to the pets registered at that time. Example: with 50 Credits and 30 pets kept for two cycles, 30 Credits are used on the first billing date and 20 on the second — the remaining 10 pets on the second date are charged. * Test pets left registered after your Credits expire become billable — **delete them first**. ### Billing cycle date [#billing-cycle-date] Your billing date is **the day of the month on which you registered your payment method**. A card registered on March 15 means the next snapshot is on April 15, and every month on the 15th after that. *** ## Quick-start checklist [#quick-start-checklist]
### Sign in to the console [#sign-in-to-the-console] Sign up and sign in at [console.petify.petnow.io](https://console.petify.petnow.io/ko). ### Register a payment method [#register-a-payment-method] Register a payment method under My Page → Payment Info. ### Issue an API Key [#issue-an-api-key] Create a key under My Page → API Key Management (grants 50 Integration Credits). ### Issue CodeArtifact credentials [#issue-codeartifact-credentials] Issue the AWS CodeArtifact credentials in My Page. ### Install and integrate the SDK [#install-and-integrate-the-sdk] Install the SDK with the issued credentials and integrate using the API Key.
*** ## Support [#support] * [support@petnow.io](mailto:support@petnow.io) # Choose an SDK Source: /petify/docs/sdk > iOS, Android, and React Native SDKs that add pet nose capture to your app Use the SDK to capture a pet's nose inside your app. Your server receives the captured images and registers, verifies, or identifies the pet through the [Server API](/petify/docs/server/api/overview). ## Versions and upgrades [#versions-and-upgrades] # Troubleshooting Source: /petify/docs/sdk/troubleshooting > Common issues and solutions when using Petnow SDK ## Overview [#overview] This document covers platform-agnostic common issues that may occur when using Petnow SDK and their solutions. For platform-specific details, please refer to each platform's documentation. ## API Authentication Issues [#api-authentication-issues] ### API Key Authentication Failure [#api-key-authentication-failure] **Symptoms** * `401 Unauthorized` when calling the API, with error code `PETNOWB2B10000` * `403 Forbidden` with `PETNOWB2B10010` if the account's API access is suspended **Causes** 1. Incorrect API Key (wrong key, or stray whitespace/special characters) 2. The API Key has been deactivated (e.g. disabled or rotated in the Petify Console). There is no Bundle ID binding or key expiry, so neither causes this error. 3. `PETNOWB2B10010` (403): the account's API access is suspended due to a billing/payment-method issue — not a key problem. **Solutions** 1. Verify the API Key — confirm it is the correct key with no stray whitespace or special characters. 2. Confirm the key is active in the [Petify Console](/petify/docs/petify-console); if it was deactivated, reissue it (create a new key) — deactivated keys cannot be reactivated. 3. For `PETNOWB2B10010`, resolve the billing/payment-method issue in the [Petify Console](/petify/docs/petify-console). ## Biometric Upload Issues [#biometric-upload-issues] ### Image Upload Failure [#image-upload-failure] **Symptoms** * Timeout or network error during file upload * The upload is rejected before a job is created **Causes** 1. File size exceeds limit (recommended: under 5MB) 2. Unstable network connection 3. Incorrect file format (only JPEG supported) **Solutions** 1. Compress images * Adjust JPEG quality to 80-90% * Adjust resolution to appropriate size (1920x1080 recommended) 2. Implement retry logic * Use exponential backoff * Recommend max 3 retries 3. Verify file format * Upload only JPEG format * Confirm file extension matches actual format ### Job Polling Timeout [#job-polling-timeout] **Symptoms** * Job status remains in `PENDING` state * Unable to receive final result **Causes** 1. Server processing delay (low image quality or complex case) 2. Incorrect Job ID 3. Polling interval too short or timeout too short **Solutions** 1. Optimize polling settings * Polling interval: 2-3 seconds recommended * Max wait time: 60 seconds recommended 2. Handle by job status * `PENDING`: Continue polling * `SUCCESS`: Process result * `FAILED`: Handle error and retry 3. Re-upload on timeout * Retry with new Job ### Capture Session Issues [#capture-session-issues] **Symptoms** * `PETNOWB2B20010` — the session id does not exist, or the session has expired * `PETNOWB2B20014` — the session already backed a register / identify / verify job **Causes** 1. Session expired 2. Reusing a session that already backed a job — a capture session is single-use 3. Incorrect session ID See [Error Codes](/petify/docs/server/api/error-codes) for the full list. **Solutions** 1. Create new capture session 2. Quickly upload and complete after session creation 3. Correctly store and pass session ID ## Pet Profile Management Issues [#pet-profile-management-issues] ### Metadata Parsing Failure [#metadata-parsing-failure] **Symptoms** * JSON parsing error when querying metadata * Specific fields are null or missing **Causes** 1. Saved in incorrect JSON format 2. Encoding/decoding mismatch 3. Special character handling error **Solutions** 1. Validate metadata on save * Verify valid JSON format * Use UTF-8 encoding 2. Implement safe parsing * Handle optional fields * Provide default values * Error handling with try-catch 3. Document metadata schema * Define required fields * Specify data types ### Partial Pet List Returned [#partial-pet-list-returned] **Symptoms** * Only some pets returned when querying pet list * Registered pet not in list **Causes** 1. Pet registered with different API Key 2. Pet was deleted 3. Server filtering (when pagination is supported in future) **Solutions** 1. Confirm using correct API Key 2. Try direct query with specific pet ID * Check existence with `GET /v2/pets/{petId}` 3. Refresh list immediately after pet registration ## Capture (SDK) Issues [#capture-sdk-issues] ### Testing with Photos or Screens [#testing-with-photos-or-screens] **Symptoms** * The camera detects the nose/face, but the capture never starts (the status stays at detection) * Pointing the camera at a photo, a printed image, or a monitor showing a dog never completes **Cause** * The SDK's fake-image classifier rejects photos, printed images, and screens. Detection reports the object, but the capture does not start because the frame is classified as not a live animal. **Solutions** 1. Test with a live animal 2. To exercise the whole pipeline without an animal, use [Web Integration trial (mock) links](/petify/docs/server/web-integration/link-integration) — their sessions skip the camera and use a bundled sample pet ### Capture Guidance by Species [#capture-guidance-by-species] **Dogs (nose print)** * Keep the camera about 30–50 cm from the nose * Use bright indoor light; avoid direct sunlight and hard shadows * Register 5 or more fingerprint (nose) images **Cats (frontal face)** * Cats are identified from the frontal face. When the head is turned sideways, tilted up, or tilted down, the SDK reports `NotFrontCatFaceHor` / `NotFrontCatFaceTop` / `NotFrontCatFaceBottom` respectively. * Instruct the handler to bring the cat's face to camera level and keep it frontal. Cat-specific distance and lighting figures and a minimum kitten age are not yet published, and identity persistence from kitten to adult has not yet been validated. If you have field data on these, please share it with [support@petnow.io](mailto:support@petnow.io). ## Identification/Verification Issues [#identificationverification-issues] ### Low Verify Accuracy (1:1 Matching) [#low-verify-accuracy-11-matching] **Symptoms** * Same pet returns `isVerified: false` * Low confidence score **Causes** 1. Image quality difference between registration and verification 2. Different capture conditions (lighting, angle, focus) 3. Insufficient registered biometric data (only 1-2 fingerprints registered) **Solutions** 1. Provide sufficient biometric data during registration * Fingerprint (nose) images: minimum 5 or more * Appearance images: captured from various angles 2. Maintain consistent capture conditions * Natural or uniform lighting * Maintain frontal angle * Sharp focus 3. Adjust confidence threshold * Set threshold according to business requirements * Implement additional verification logic for low confidence 4. A high similarity score against a different pet can occur; combine the score with your own context (species, metadata, user confirmation) ### Too Many Identify Results (1:N Matching) [#too-many-identify-results-1n-matching] **Symptoms** * Identification results include unrelated pets * Low confidence for Top 1 result **Causes** 1. Many pets with similar appearance in database 2. Low uploaded image quality 3. Threshold setting too low **Solutions** 1. Filter by confidence * Display only top N (e.g., Top 3) * Apply minimum confidence threshold (e.g., 70 or above) 2. Filter with additional metadata * Pre-filter by species, breed, region, etc. 3. Add user confirmation process * Show results to user and let them select 4. A high similarity score against a different pet can occur; combine the score with your own context (species, metadata, user confirmation) ## Network Issues [#network-issues] ### Intermittent Timeouts [#intermittent-timeouts] **Symptoms** * API calls occasionally timeout * Failures even with good network status **Causes** 1. Server load 2. Client timeout setting too short 3. Proxy or firewall configuration **Solutions** 1. Increase timeout * Request timeout: 30 seconds or more * Resource timeout: 60 seconds or more 2. Implement retry logic * Use exponential backoff * Set max retry count 3. Check network status before request * Verify network connectivity * Monitor connection state changes ### CORS Error (Web Environment) [#cors-error-web-environment] **Symptoms** * CORS policy violation error in browser * `Access-Control-Allow-Origin` related error **Cause** * CORS restriction when calling API directly from web environment **Solutions** 1. Use backend proxy * Client → Your backend → Petnow API 2. Request domain registration from Petnow representative * Request to add allowed Origin ## Performance and Optimization [#performance-and-optimization] ### Excessive API Calls [#excessive-api-calls] **Symptoms** * Too many API calls in short time * Unnecessary server load and cost **Causes** 1. Unnecessary duplicate calls 2. Polling interval too short 3. Not using caching **Solutions** 1. Response caching * Use local cache for pet list, pet info * Set TTL (e.g., 5 minutes) 2. Optimize polling * Job polling interval: 2-3 seconds * Limit max polling count 3. Apply Debouncing/Throttling * Prevent consecutive requests ### Increasing Memory Usage [#increasing-memory-usage] **Symptoms** * Memory leak after image upload * App gradually slows down **Causes** 1. Image data kept in memory continuously 2. Unlimited cache growth 3. Missing resource cleanup **Solutions** 1. Release immediately after image processing * Release original data after upload completion * Delete temporary files 2. Limit cache size * Set max cache size (e.g., 100MB) * Use LRU policy 3. Handle memory warnings * Clear cache on system memory warning ## Error Codes [#error-codes] Server API errors are returned with `PETNOWB2B...` codes and an HTTP status. For the full list of codes with causes and solutions, see [Server API – Error Codes](/petify/docs/server/api/error-codes). ## Debugging Tips [#debugging-tips] ### 1. Check Error Response Details [#1-check-error-response-details] ```json { "error": { "code": "ERROR_CODE", "message": "Human readable message", "field": "problematic_field", "details": { "additional": "info" } } } ``` ### 3. Monitor Network Traffic [#3-monitor-network-traffic] * Check network requests in developer tools * Inspect request/response headers, body * Analyze timing information ### 4. Collect Logs [#4-collect-logs] * Record API call time, parameters, response * Save full context when error occurs * Create reproducible test cases ## Support Request [#support-request] If the problem is not resolved with the above solutions, please contact Petnow representative with the following information: **Required Information** * API Key (or masked form) * Occurrence time (specify UTC or local timezone) * Request parameters (excluding sensitive information) * Error message and HTTP status code * SDK version and platform information **Optional Information** * Reproduction steps * Screenshots or log files * Network environment (WiFi, 4G/5G, etc.) * Difference between expected and actual behavior **Contact** * Email: [support@petnow.io](mailto:support@petnow.io) * Direct contact with representative (contact provided upon contract) # Choose an integration Source: /petify/docs/server > Call the Petnow API from your server, or provide the capture flow through a web link There are two ways to integrate on the server side. The server docs describe how the production server behaves today. Unlike the SDK, they are not versioned. # Basic Usage Source: /petify/docs/sdk/android/basic-usage > How to integrate the camera UI with CameraView and CameraController. *** ## Before You Begin [#before-you-begin] Before starting this guide, complete [Getting Started](/petify/docs/sdk/android/getting-started). You need to prepare your license (`LicenseInfo`) and detection settings (`DetectionConfiguration`). The recommended integration approach is to use **`CameraView` (a `FrameLayout` widget that draws the preview plus the default tracking UI)** and **`CameraController` (the camera/detection session engine)** directly. Because it does not require subclassing a Fragment, it integrates naturally into plain View screens, Jetpack Compose, and custom UIs alike. If you are already using the Fragment hosting approach (`PetnowCameraFragment`), see the [Fragment Approach (Legacy)](/petify/docs/sdk/android/fragment) document.
### Place the camera view [#place-the-camera-view] Add `CameraView` to your layout or Compose. ### Create and connect the controller [#create-and-connect-the-controller] Create a `CameraController`, register a listener, then connect it to `CameraView`. ### Start the session [#start-the-session] Check the camera permission and call `initializeCamera()` → `startDetection()`. ### Handle results [#handle-results] Handle the capture-completed/failed callbacks. ### Clean up resources [#clean-up-resources] Clean up the controller when the screen is dismissed.

*** ## Step 1: Place the Camera View [#step-1-place-the-camera-view] `CameraView` is a widget that extends `FrameLayout`. Add it directly to an XML layout, or wrap it with `AndroidView` in Compose. **XML layout** ```xml ``` **Jetpack Compose** ```kotlin import androidx.compose.ui.viewinterop.AndroidView import io.petnow.ui.CameraView AndroidView( factory = { ctx -> CameraView(ctx) }, modifier = Modifier.fillMaxSize(), update = { view -> view.controller = controller }, // The controller from Step 2 ) ``` `CameraView` automatically renders the camera preview and the default tracking UI (nose/face trackers). It also manages the preview Surface internally, so all you need to do is connect a controller. *** ## Step 2: Create and Connect the Controller [#step-2-create-and-connect-the-controller] Create a `CameraController`, register a detection event listener, and then connect it to `CameraView`. ```kotlin import io.petnow.ui.CameraController import io.petnow.ui.CameraResult import io.petnow.ui.config.LicenseInfo import io.petnow.ui.status.DetectionStatus import io.petnow.callback.PetnowCameraDetectionListenerV2 // Use a CoroutineScope tied to the screen lifecycle, such as lifecycleScope. val controller = CameraController( // [!code highlight] context.applicationContext, // [!code highlight] LicenseInfo(apiKey = "YOUR_API_KEY"), // [!code highlight] lifecycleScope, // [!code highlight] ) // [!code highlight] controller.setDetectionListenerV2(object : PetnowCameraDetectionListenerV2 { // [!code highlight] override fun onDetectionStatus(primaryDetectionStatus: DetectionStatus) { // Implemented in Step 5 — real-time guidance } override fun onDetectionProgress(progress: Int) { // Implemented in Step 5 — progress 0–100 } override fun onDetectionFinished(result: CameraResult) { // Implemented in Step 4 — final result } }) // Connecting it to CameraView starts the preview. cameraView.controller = controller // [!code highlight] ``` ### Understanding the Elements [#understanding-the-elements] | Element | Description | | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CameraController(context, license, scope)` | The camera/detection session engine. `scope` is a `CoroutineScope` (e.g. `lifecycleScope`) that stays valid for the duration of the session | | `LicenseInfo(apiKey, environment)` | License information (also used for monitoring/metrics). `environment: PetnowEnvironment?` (`PRODUCTION` / `STAGING`) is optional — omit it to keep the environment the SDK was built for, which is **production** for every published build. A staging-console key is only valid against the staging backend and vice versa; a mismatch fails license validation. Do not wire it to `BuildConfig.DEBUG`. (Restores the capability v1.3.x had as `isDebugMode`, removed in v1.4.) | | `setDetectionListenerV2(...)` | Registers callbacks for detection status, progress, and the final result | | `cameraView.controller = ...` | Connects the controller to the view (automatically supplies the preview Surface) | Detection settings (`species`/`purpose`/`enableFakeDetection`, and optionally `inferenceTier`) are passed via `DetectionConfiguration` in the next step's `initializeCamera()`, not at controller creation. `enableFakeDetection` defaults to `true`: spoof detection (photos, screens, printed images) is on. Passing `false` turns spoof detection off for that session, which is how you evaluate the SDK against photographs of pets instead of live animals. `inferenceTier` (`AUTO` default / `CPU` / `GPU`) picks the detector path; leave it at `AUTO` unless support asks you to force a tier. It is read **once**, when the process creates its first detector engine, and fixed from then on: a later controller asking for the other tier keeps running on the first decision (the SDK logs a warning). Set it on the first `initializeCamera()` of the process, or restart the app to change it. *** ## Step 3: Start the Session [#step-3-start-the-session] After checking the camera permission, prepare the session with `initializeCamera()` and start detection with `startDetection()`. The two calls are **separate** — `initializeCamera()` only opens the camera, and detection does not begin until you call `startDetection()`. ```kotlin import io.petnow.ui.config.DetectionConfiguration import io.petnow.ui.config.DetectionPurpose import io.petnow.ui.config.PetSpecies import java.util.UUID val configuration = DetectionConfiguration( species = PetSpecies.DOG, purpose = DetectionPurpose.PET_PROFILE_REGISTRATION, enableFakeDetection = true, ) // captureSessionId issued by the server (see Getting Started) val captureSessionId: UUID = /* captureSessionId received from the server */ lifecycleScope.launch { // CameraView does not request permissions itself — the host must check first. if (!hasCameraPermission()) { requestCameraPermission() return@launch } controller.initializeCamera(configuration, captureSessionId) // [!code highlight] controller.startDetection() // [!code highlight] } ``` Unlike `PetnowCameraFragment`, `CameraView` does not automatically request the camera permission. Declare `` in `AndroidManifest.xml` and request the runtime permission yourself before starting the session. `initializeCamera()` is a `suspend` function that returns only after the preview Surface is ready and the camera has opened for the first time. The `startDetection()` call immediately afterward is therefore always safe. Android's `initializeCamera()` has no camera-facing argument. It **always starts with the back camera**; toggle front/back with `switchCamera()` (there is no option to *start* on the front camera). This differs from iOS's `initializeCamera(initialPosition:)`. *** ## Step 4: Handle Capture Results [#step-4-handle-capture-results] When detection completes, `onDetectionFinished(result: CameraResult)` is called. ```kotlin override fun onDetectionFinished(result: CameraResult) { when (result) { is CameraResult.Success -> { // [!code highlight] // result.fingerprintImageFiles: biometric images (dog = nose, cat = face) // result.appearanceImageFiles: appearance (face) images uploadImages(result.fingerprintImageFiles, result.appearanceImageFiles) } is CameraResult.Fail -> { // [!code highlight] // Capture failed — retry or end the session showRetryOrExitDialog() } } } ``` ### The CameraResult Type [#the-cameraresult-type] ```kotlin sealed class CameraResult { data class Success( val fingerprintImageFiles: List, // biometric (dog = nose, cat = face) val appearanceImageFiles: List // appearance (face) images ) : CameraResult() data object Fail : CameraResult() } ``` Image files are saved to the app's cache directory under `file://` paths. The recommended flow is to upload these files to your app server, which then performs registration/verification/identification via the Petnow Server API. For the upload endpoints (`/v2/fingerprints:upload`, `/v2/appearances:upload`) and the full flow, see [Server API – Biometric](/petify/docs/server/api/biometric). ### Handling Failure (Fail) [#handling-failure-fail] `CameraResult.Fail` is delivered when not enough images could be captured within the capture time window (e.g. the pet left the frame, lighting was inadequate, etc.). ```kotlin is CameraResult.Fail -> { AlertDialog.Builder(requireContext()) .setTitle("Capture failed") .setMessage("We couldn't capture the nose print clearly. Would you like to try again?") .setPositiveButton("Retry") { _, _ -> controller.startDetection() // Restart from the beginning in the same session // [!code highlight] } .setNegativeButton("Cancel") { _, _ -> navigateBack() } .show() } ``` Even after receiving `Fail`, the server's capture session is still open. If you close the screen without retrying, the server automatically ends (ABORTED) the session after about 5 minutes. *** ## Step 5: Observe Status and Progress [#step-5-observe-status-and-progress] ### Progress [#progress] ```kotlin override fun onDetectionProgress(progress: Int) { // [!code highlight] // progress: 0–100 progressBar.progress = progress } ``` ### Detection Status (Real-Time Guidance) [#detection-status-real-time-guidance] The `DetectionStatus` delivered through `onDetectionStatus` is a sealed class. When it is `Failed`, `reason` tells you the specific cause. ```kotlin override fun onDetectionStatus(primaryDetectionStatus: DetectionStatus) { // [!code highlight] statusTextView.text = when (primaryDetectionStatus) { DetectionStatus.NoObject -> "Fit your pet within the frame" DetectionStatus.Processing -> "Detecting..." DetectionStatus.Detected -> "Great! Hold still" DetectionStatus.Finished -> "Capture complete" is DetectionStatus.Failed -> when (primaryDetectionStatus.reason) { // [!code highlight] DetectionFailureReason.TooClose -> "Move a little farther away" DetectionFailureReason.TooFarAway -> "Move a little closer" DetectionFailureReason.TooDark -> "Move to a brighter spot" DetectionFailureReason.NotFrontFace -> "Point the face straight at the camera" else -> "Adjust the pose" } } } ``` ```kotlin import io.petnow.ui.status.DetectionStatus import io.petnow.ui.status.DetectionFailureReason sealed class DetectionStatus { data object NoObject : DetectionStatus() // No pet detected in the frame data object Processing : DetectionStatus() // Detection in progress data object Detected : DetectionStatus() // Biometric features detected data object Finished : DetectionStatus() // Capture complete data class Failed(val reason: DetectionFailureReason) : DetectionStatus() } ``` **`DetectionFailureReason` values** | Category | Values | | --------------------- | ---------------------------------------------------------------------------------------------------------------------- | | General | `Error`, `Unexpected` | | Distance | `TooClose`, `TooFarAway` | | Lighting | `TooBright`, `TooDark`, `ShadowDetected`, `GlareDetected` | | Sharpness | `TooBlurred`, `MotionBlurDetected`, `DefocusedBlurDetected` | | Angle | `NotFrontFace`, `NotFrontNose`, `NotFrontNoseTop`, `NotFrontCatFaceHor`, `NotFrontCatFaceTop`, `NotFrontCatFaceBottom` | | Occlusion/obstruction | `NoseNotFound`, `FurDetected` | | Tampering/spoofing | `FakeDetected`, `HumanFaceDetected` | ### Observing via StateFlow (Optional) [#observing-via-stateflow-optional] Instead of callbacks, you can also observe state via the `StateFlow` returned by the `state` property. This is useful for Compose or unidirectional data flow. ```kotlin lifecycleScope.launch { controller.state.collect { state -> // state.progressPercent: progress (0–100) // state.detectionStatusList: list of detection statuses for the current frame // state.currentDetectionResult: nose/face BoundingBox (DetectionResult?) // state.isDetectionFinished: whether detection is complete } } ``` The nose/face position of a detection result (`DetectionResult` = `nose`/`face` `BoundingBox`) is provided through `state.currentDetectionResult`, not the V2 listener. ### Performance Stats (Optional) [#performance-stats-optional] `controller.performanceStats` is a `StateFlow` with live figures for diagnostics screens and support reports. It is not needed for the capture flow itself. ```kotlin lifecycleScope.launch { controller.performanceStats.collect { s -> // s.cameraInitMs: initializeCamera() duration (null until the camera opened) // s.firstFrameMs: time to the first processed detector frame // s.fps / s.frameIntervalMs: processed-frame rate over the last ~30 frames // s.detectorMs: detector time of the latest frame (smoothed) // s.inferenceTier: "cpu" or "gpu", s.inferenceTierReason: why it was chosen } } ``` *** ## Step 6: Clean Up Resources [#step-6-clean-up-resources] Clean up the controller when the screen is dismissed. `finalizeCamera()` stops the camera session (the controller can be reused), while `release()` fully releases native resources (detector/sound pool) as well (terminates). ```kotlin override fun onDestroy() { super.onDestroy() controller.release() // When leaving the screen for good // [!code highlight] } ``` In Compose, clean up in `onDispose` of a `DisposableEffect`. ```kotlin DisposableEffect(Unit) { onDispose { controller.release() } } ``` *** ## Additional Features [#additional-features] ### Switching Between Front/Back Cameras [#switching-between-frontback-cameras] ```kotlin import io.petnow.ui.camera.cameraInfo if (context.cameraInfo.isFrontCameraSupported) { controller.switchCamera() // Returns a Result // [!code highlight] .onFailure { e -> Toast.makeText(context, "Switch failed: ${e.message}", Toast.LENGTH_SHORT).show() } } else { Toast.makeText(context, "This device does not support a front camera", Toast.LENGTH_SHORT).show() } ``` ### Pausing / Resuming Detection [#pausing--resuming-detection] You can pause detection briefly while keeping the camera preview running. This is useful while displaying a tip dialog. ```kotlin controller.pauseDetection() // Pause (keeps progress/captured images, returns nothing) controller.resumeDetection() // Resume from where it was paused (returns a Result) ``` ### Re-Capturing (From the Beginning) [#re-capturing-from-the-beginning] ```kotlin controller.startDetection() // Reset progress to 0 and start a new detection session (returns a Result) ``` **Differences between `startDetection` / `pauseDetection` / `resumeDetection`** * `startDetection()` — Resets progress to 0 and starts a new detection session (re-capture). Returns a `Result`. * `pauseDetection()` — Pauses detection only. Keeps the preview/progress. Returns nothing. * `resumeDetection()` — Resumes from where it was paused. Returns a `Result`. *** ## Error Handling [#error-handling] `initializeCamera()` (suspend) throws initialization failures as the sealed class `PetnowUIError`, while `startDetection()` / `resumeDetection()` / `switchCamera()` return `Result`. Handle each. `initializeCamera()` validates the API key against the server before opening the camera (once per key per process — an already-validated key does not re-hit the network). A rejected key fails with `PetnowUIError.InvalidLicense`; the camera is not opened and detection never starts. The SDK uses **one API key per process**. Calling `initializeCamera()` with a second, different key fails with `PetnowUIError.MultipleApiKeys` instead of silently replacing the first one — the first key keeps working. The console can issue several keys, but one process must use one of them; restart the app to run against another. ```kotlin import io.petnow.ui.PetnowCaptureException import io.petnow.ui.PetnowUIError import kotlinx.coroutines.CancellationException lifecycleScope.launch { try { controller.initializeCamera(configuration, captureSessionId) } catch (c: CancellationException) { throw c // cancelled by a screen change/reconfig — normal lifecycle, rethrow } catch (e: PetnowUIError) { when (e) { // sealed — these cases cover every initialization failure is PetnowUIError.InvalidLicense -> { /* API key rejected by the server — check the key */ } is PetnowUIError.MultipleApiKeys -> { /* a different API key was already used in this process */ } is PetnowUIError.PermissionDenied -> { /* camera permission missing — request it or guide to Settings */ } is PetnowUIError.CameraOpenFailed -> { /* camera could not be opened — see e.cause */ } } return@launch } controller.startDetection() // [!code highlight] .onSuccess { /* detection started */ } .onFailure { e -> handleDetectionError(e) } } private fun handleDetectionError(e: Throwable) { when (e) { is PetnowCaptureException.InvalidState -> { /* controller not initialized yet */ } is PetnowCaptureException.AlreadyCapturing -> { /* detection already running */ } is PetnowCaptureException.NotSupported -> { /* not supported on this device/config */ } else -> { /* other (e.g. PetnowCaptureException.Unknown) */ } } } ``` ### Exception / result types [#exception--result-types] | Call | Failure shape | Main types | | ---------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `initializeCamera()` | throws | `PetnowUIError` (`InvalidLicense` license rejected / `MultipleApiKeys` a different API key already used in this process / `PermissionDenied` permission missing / `CameraOpenFailed` camera open failure — original cause in `cause`), `CancellationException` (normal cancel) | | `startDetection()` / `resumeDetection()` | `Result.failure` | `PetnowCaptureException` (`InvalidState`/`AlreadyCapturing`/`NotSupported`/`Unknown`) | | `switchCamera()` | `Result.failure` | `PetnowCaptureException` (`NotSupported` no front camera / `InvalidState` not initialized / `Unknown` other — original cause in `cause`) | `PetnowUIError` (`io.petnow.ui.PetnowUIError`) and `PetnowCaptureException` (`io.petnow.ui.PetnowCaptureException`) are both sealed classes — branch on their cases with `when`. The three main initialization failures use the same names on both platforms (`invalidLicense` / `multipleApiKeys` / `permissionDenied`). Camera-open failure is named differently: Android `CameraOpenFailed`, iOS `cameraUnavailable`. *** ## Best Practices [#best-practices] ### Managing the API Key Safely [#managing-the-api-key-safely] ```kotlin // ❌ Bad: hardcoded in code val apiKey = "sk_live_abc123..." // ✅ Good: use BuildConfig or local.properties val apiKey = BuildConfig.PETNOW_API_KEY ``` Do not write the API key directly in your source code. We recommend injecting it via `local.properties` or CI/CD environment variables. ### Cleaning Up Resources [#cleaning-up-resources] Always call `controller.release()` when leaving the screen. If you don't, the camera and native detector resources won't be released, which can cause problems in the next session. *** ## Troubleshooting [#troubleshooting] ### Q. The camera won't start [#q-the-camera-wont-start] **A.** Check the following: 1. Whether the camera permission (`CAMERA`) is declared in `AndroidManifest.xml` 2. Whether you requested the runtime permission **before** starting the session (`CameraView` does not request it itself) 3. Whether you connected the controller with `cameraView.controller = controller` 4. Whether you called `startDetection()` **after** `initializeCamera()` ### Q. Capture keeps failing [#q-capture-keeps-failing] **A.** Review the capture environment: 1. Capture under bright indoor lighting (avoid direct sunlight and shadows) 2. Keep a distance of 30–50 cm between the camera and the pet's nose 3. Keep the pet still using treats or similar 4. Display the status delivered through `onDetectionStatus` in the UI to guide the user ### Q. The preview froze after switching screens [#q-the-preview-froze-after-switching-screens] **A.** If you leave the screen without cleaning up the controller, the camera may not open in the next session. Call `controller.release()` when the screen is dismissed. *** ## Next Steps [#next-steps] Once you've learned the basics, see the following documents: * [Fully Custom UI](/petify/docs/sdk/android/custom-ui) - Build a 100% custom UI with `CameraController` + your own SurfaceView * [Customization](/petify/docs/sdk/android/customization) - Customize the `CameraView` guide UI * [Sound Guide](/petify/docs/sdk/android/sound) - Configuring sound playback * [Fragment Approach (Legacy)](/petify/docs/sdk/android/fragment) - The existing `PetnowCameraFragment` integration approach # Fully Custom UI Source: /petify/docs/sdk/android/custom-ui > How to build a 100% custom camera UI with CameraController and your own SurfaceView. *** Before starting this guide, read the [UI Module Overview](/petify/docs/sdk/android/overview) and [Basic Usage](/petify/docs/sdk/android/basic-usage). ## Overview [#overview] The `CameraView` from [Basic Usage](/petify/docs/sdk/android/basic-usage) draws both the preview and the default tracking UI for you. By contrast, **if you want to draw everything yourself, including the tracking overlay**, you can use only `CameraController` without `CameraView` and **provide the preview Surface yourself**. This approach is well suited to the following cases: * A fully custom screen based on **Jetpack Compose** * Cross-platform bridges such as **React Native** (the Petnow React Native package uses this approach) * Cases where you need to draw your own overlay instead of using the SDK's default tracking UI ### CameraView Approach vs. Fully Custom Approach [#cameraview-approach-vs-fully-custom-approach] | Feature | CameraView (default) | Fully custom | | ------------------- | ---------------------------------- | --------------------------------------------------------------- | | Preview Surface | Managed internally by `CameraView` | Create your own `SurfaceView` and call `attachPreviewSurface()` | | Default tracking UI | Built-in | None — implement it yourself | | Camera permission | Handled by the host | Handled by the host | | Receiving status | V2 listener / the `state` property | V2 listener / the `state` property | | UI flexibility | Add overlays | 100% custom | In the fully custom approach, the SDK does not draw any UI. You must render progress, status, and tracking boxes yourself from the `state` property or the V2 listener values. *** ## Architecture [#architecture] `CameraController` manages the entire lifecycle of the camera/detection but **is not involved in UI rendering**. You draw the preview with your own `SurfaceView` and receive status through the `StateFlow` (the `state` property) and the `PetnowCameraDetectionListenerV2` callbacks. *** ## Basic Integration [#basic-integration] ### Step 1: Create the Controller + Register a Listener [#step-1-create-the-controller--register-a-listener] ```kotlin import io.petnow.ui.CameraController import io.petnow.ui.CameraResult import io.petnow.ui.config.LicenseInfo import io.petnow.ui.status.DetectionStatus import io.petnow.callback.PetnowCameraDetectionListenerV2 val controller = CameraController( context.applicationContext, LicenseInfo(apiKey = "YOUR_API_KEY"), scope, ).apply { setDetectionListenerV2(object : PetnowCameraDetectionListenerV2 { override fun onDetectionStatus(primaryDetectionStatus: DetectionStatus) { /* render it yourself */ } override fun onDetectionProgress(progress: Int) { /* 0~100 */ } override fun onDetectionFinished(result: CameraResult) { /* handle the result */ } }) } ``` ### Step 2: Provide the SurfaceView + Start the Session [#step-2-provide-the-surfaceview--start-the-session] Pass the Surface of your own `SurfaceView` to `attachPreviewSurface()`. The camera opens once the Surface is ready. Since `initializeCamera()` waits until the camera is open, you can call `startDetection()` afterward. The samples below pass `enableFakeDetection = false` explicitly to turn spoof detection off; the default is `true`. See [Getting Started](/petify/docs/sdk/android/getting-started#parameter-reference) for the details. ```kotlin import android.view.SurfaceHolder import android.view.SurfaceView import io.petnow.ui.config.DetectionConfiguration import io.petnow.ui.config.DetectionPurpose import io.petnow.ui.config.PetSpecies import java.util.UUID val config = DetectionConfiguration( species = PetSpecies.DOG, purpose = DetectionPurpose.PET_PROFILE_REGISTRATION, enableFakeDetection = false, ) val captureSessionId: UUID = /* captureSessionId received from the server */ val surfaceView = SurfaceView(context).apply { holder.addCallback(object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) { controller.attachPreviewSurface(holder.surface) // [!code highlight] scope.launch { controller.initializeCamera(config, captureSessionId) // [!code highlight] controller.startDetection() // [!code highlight] } } override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, ht: Int) {} override fun surfaceDestroyed(holder: SurfaceHolder) { controller.detachPreviewSurface() // camera closes // [!code highlight] } }) } ``` `CameraController` does not request the camera permission. Declare the `CAMERA` permission in `AndroidManifest.xml` and request the runtime permission yourself before starting the session. ### Step 3: Observe Status (StateFlow) [#step-3-observe-status-stateflow] ```kotlin scope.launch { controller.state.collect { state -> // state.progressPercent: progress (0~100) // state.detectionStatusList: list of detection statuses for the current frame (PetnowDetectionStatus) // state.currentDetectionResult: nose/face BoundingBox (DetectionResult?) — for rendering the tracking box // state.isDetectionFinished: whether detection is complete } } ``` ### Step 4: Release Resources [#step-4-release-resources] ```kotlin controller.release() // fully releases native detector/sound pool (terminates) ``` `finalizeCamera()` stops only the camera session and lets the controller be reused. When you leave the screen for good, clean up the native resources too with `release()`. *** ## Jetpack Compose Example [#jetpack-compose-example] ```kotlin @Composable fun PetnowCameraScreen(captureSessionId: UUID) { val context = LocalContext.current val scope = rememberCoroutineScope() val controller = remember { CameraController( context.applicationContext, LicenseInfo(apiKey = "YOUR_API_KEY"), scope, ) } val state by controller.state.collectAsState() val config = remember { DetectionConfiguration( species = PetSpecies.DOG, purpose = DetectionPurpose.PET_PROFILE_REGISTRATION, enableFakeDetection = false, ) } LaunchedEffect(Unit) { controller.setDetectionListenerV2(object : PetnowCameraDetectionListenerV2 { override fun onDetectionStatus(primaryDetectionStatus: DetectionStatus) { /* ... */ } override fun onDetectionProgress(progress: Int) { /* ... */ } override fun onDetectionFinished(result: CameraResult) { /* ... */ } }) } // The screen owns the controller, so dispose = full release(). DisposableEffect(Unit) { onDispose { controller.release() } } Column(modifier = Modifier.fillMaxSize()) { // Preview: wrap your own SurfaceView in an AndroidView. AndroidView( factory = { ctx -> SurfaceView(ctx).apply { holder.addCallback(object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) { controller.attachPreviewSurface(holder.surface) scope.launch { controller.initializeCamera(config, captureSessionId) controller.startDetection() } } override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, ht: Int) {} override fun surfaceDestroyed(holder: SurfaceHolder) { controller.detachPreviewSurface() } }) } }, modifier = Modifier.weight(1f), ) // Progress bar (rendered directly from state) LinearProgressIndicator( progress = { state.progressPercent / 100f }, modifier = Modifier.fillMaxWidth(), ) // Detection status text Text( text = state.detectionStatusList.firstOrNull()?.name ?: "", modifier = Modifier.padding(16.dp), textAlign = TextAlign.Center, ) } } ``` *** ## Camera Switch / Pause / Resume [#camera-switch--pause--resume] Uses the same controller API as the `CameraView` approach. ```kotlin controller.switchCamera() // returns a Result (front ↔ back) controller.pauseDetection() // pause (returns nothing) controller.resumeDetection() // resume (returns a Result) controller.startDetection() // re-capture: reset progress and start a new session (returns a Result) ``` *** ## Sound Playback [#sound-playback] ```kotlin import io.petnow.ui.sound.SoundPlayer import io.petnow.ui.sound.SoundType val sounds = SoundPlayer(context) lifecycleScope.launch { sounds.prepare() // load the samples first val handle = sounds.play(SoundType.RANDOM) // -1 until prepare() finishes // … sounds.stop(handle) } // when the screen goes away sounds.release() ``` Sound playback is independent of the camera, so `SoundPlayer` does not need a `CameraController`. `CameraController.playSound()` / `stopSound()` are deprecated since 1.5.0. For details on the available sound types, see the [Sound Guide](/petify/docs/sdk/android/sound). *** ## API Summary [#api-summary] | Method | Description | | -------------------------------------------- | --------------------------------------------------------------- | | `CameraController(context, license, scope)` | Create the controller | | `setDetectionListenerV2(listener)` | Register callbacks for detection status/progress/result | | `attachPreviewSurface(surface)` | Connect your own Surface (camera opens) | | `detachPreviewSurface()` | Detach the Surface (camera closes) | | `initializeCamera(config, captureSessionId)` | Prepare the session (suspend) | | `startDetection()` | Start/restart detection (`Result`) | | `pauseDetection()` / `resumeDetection()` | Pause / resume detection | | `switchCamera()` | Switch front/back (`Result`) | | the `state` property | Observe `StateFlow` | | `setBracketingMode(enabled)` | Configure bracketing mode | | `playSound(type)` / `stopSound(id)` | Play/stop sound — **deprecated since 1.5.0**, use `SoundPlayer` | | `finalizeCamera()` | Stop the camera session (reusable) | | `release()` | Fully release native resources (terminate) | *** ## Next Steps [#next-steps] * [Customization](/petify/docs/sdk/android/customization) - Customize the `CameraView` guide UI * [Sound Guide](/petify/docs/sdk/android/sound) - Detailed guide to sound playback # UI Customization Source: /petify/docs/sdk/android/customization > How to customize the CameraView guide UI and tracking display. *** Before starting this guide, complete [Basic Usage](/petify/docs/sdk/android/basic-usage). ## Overview [#overview] `CameraView` draws the preview and the default tracking UI. On top of it, you can customize the following: 1. **Build the surrounding UI yourself** — Place status/progress text, buttons, and so on outside of `CameraView` as regular Views/Compose, and drive them with the listener and the `state` property 2. **Floating guide** — Inject a guide view that moves along with the tracker (`setFloatingGuide`) 3. **Toggle the tracking display** — Whether to show the default nose tracking UI (`isDogNoseTrackingUiEnabled`) The approach of **injecting a custom overlay layout** on top of the default tracking UI (`provideCustomOverlayLayout`, etc.) is exclusive to `PetnowCameraFragment`. For that approach, see [Fragment Method (Legacy)](/petify/docs/sdk/android/fragment#ui-customization). To draw 100% of the tracking UI yourself, use [Fully Custom UI](/petify/docs/sdk/android/custom-ui). *** ## Build the Surrounding UI Yourself [#build-the-surrounding-ui-yourself] Since `CameraView` is only responsible for the preview area, you can place status text, progress bars, buttons, and so on as regular layouts above or around it, and update them via the detection callbacks. ```xml ``` ```kotlin controller.setDetectionListenerV2(object : PetnowCameraDetectionListenerV2 { override fun onDetectionStatus(primaryDetectionStatus: DetectionStatus) { binding.statusText.text = when (primaryDetectionStatus) { DetectionStatus.NoObject -> "Align your pet's face in the frame" DetectionStatus.Detected -> "Nose print scans are coming through!" is DetectionStatus.Failed -> when (primaryDetectionStatus.reason) { DetectionFailureReason.TooClose -> "It's too close! Move back a bit." DetectionFailureReason.TooFarAway -> "It's too far! Move closer." DetectionFailureReason.TooDark -> "It's too dark! Try a brighter place." else -> "Adjust your pet's pose" } else -> "" } } override fun onDetectionProgress(progress: Int) { binding.detectionProgress.progress = progress } override fun onDetectionFinished(result: CameraResult) { /* ... */ } }) ``` *** ## Floating Guide (A Guide That Follows the Tracker) [#floating-guide-a-guide-that-follows-the-tracker] Inject a guide view that moves along with the tracking area using `setFloatingGuide()`. `CameraView` automatically handles the coordinate and position calculations. ```kotlin val guide = layoutInflater.inflate(R.layout.layout_floating_guide, cameraView, false) cameraView.setFloatingGuide(guide) // [!code highlight] // Control visibility and text yourself, in line with the detection status val message = guide.findViewById(R.id.text_guide_message) message.text = "Point the nose straight at the camera" ``` The guide view is placed with `WRAP_CONTENT` by default. Update its visibility and content from your app in line with the detection status (`onDetectionStatus` or the `state` property). To remove it, call `setFloatingGuide(null)`. *** ## Toggling the Tracking UI Display [#toggling-the-tracking-ui-display] To turn off the default nose tracking display, set `isDogNoseTrackingUiEnabled` to `false` (useful when you draw the tracking UI yourself). ```kotlin cameraView.isDogNoseTrackingUiEnabled = false // [!code highlight] ``` *** ## Next Steps [#next-steps] Once you've learned how to customize the UI, refer to the following documentation: * [Fully Custom UI](/petify/docs/sdk/android/custom-ui) - Implement 100% of the tracking UI yourself * [Sound Guide](/petify/docs/sdk/android/sound) - Sound playback settings * [Fragment Method (Legacy)](/petify/docs/sdk/android/fragment) - The Fragment overlay layout injection approach # Fragment Approach (Legacy) Source: /petify/docs/sdk/android/fragment > The existing PetnowCameraFragment hosting approach and the 1.4.0 migration guide. *** `PetnowCameraFragment` is the existing (legacy) hosting approach. **For new integrations, we recommend [`CameraView` + `CameraController`](/petify/docs/sdk/android/basic-usage).** This document is provided for maintaining projects that already use the Fragment approach and for migrating to 1.4.0. The class and its main methods carry `@Deprecated`, so building against them raises deprecation warnings. They keep working and may be removed in a future major release. ## 1.4.0 Changes (Migration) [#140-changes-migration] In 1.4, the global entry point (v1.3.x's `PetnowApiClient`) is no longer shipped. The license and detection settings are now **passed directly as Fragment arguments**. | Before (1.3.x and earlier) | 1.4.0 | | ------------------------------------------------------ | ------------------------------------------------------------ | | `PetnowApiClient.init(key, isDebugMode)` (Application) | Fragment args `ARG_API_KEY` (or override `provideLicense()`) | | `PetnowApiClient.configureDetectionMode(...)` | Fragment args `ARG_DETECTION_CONFIGURATION` (Serializable) | | `PetnowApiClient.isSuccessInitialize` | Removed — failures surface as `PetnowUIError` | | `import io.petnow.ui.PetnowCameraDetectionListener` | `import io.petnow.callback.PetnowCameraDetectionListener` | `PetnowCameraFragment` uses the V1 listener (`PetnowCameraDetectionListener` — `PetnowDetectionStatus` / `DetectionCaptureResult`). Its types differ from the V2 listener (`DetectionStatus` / `CameraResult`) used by the `CameraController` approach. *** ## Step 1: Create the Camera Fragment [#step-1-create-the-camera-fragment] Subclass `PetnowCameraFragment` and, in `onCreate()`, set the license, settings, and session ID as arguments **before calling `super.onCreate()`**. ```kotlin import android.os.Bundle import android.content.Context import io.petnow.ui.PetnowCameraFragment import io.petnow.ui.config.DetectionConfiguration import io.petnow.ui.config.DetectionPurpose import io.petnow.ui.config.PetSpecies import io.petnow.callback.PetnowCameraDetectionListener import io.petnow.ui.DetectionCaptureResult import io.petnow.ui.status.PetnowDetectionStatus import java.util.UUID class ClientCameraFragment : PetnowCameraFragment(), PetnowCameraDetectionListener { companion object { fun newInstance(captureSessionId: UUID) = ClientCameraFragment().apply { arguments = Bundle().apply { putString(ARG_CAPTURE_SESSION_ID, captureSessionId.toString()) } } } override fun onCreate(savedInstanceState: Bundle?) { // Inject the license/detection settings as args before super.onCreate(). // [!code highlight] arguments = Bundle(arguments ?: Bundle()).apply { putString(ARG_API_KEY, "YOUR_API_KEY") // license // [!code highlight] putSerializable( // detection settings // [!code highlight] ARG_DETECTION_CONFIGURATION, // [!code highlight] DetectionConfiguration( // [!code highlight] species = PetSpecies.DOG, // [!code highlight] purpose = DetectionPurpose.PET_PROFILE_REGISTRATION, // [!code highlight] enableFakeDetection = true, // [!code highlight] ), // [!code highlight] ) } super.onCreate(savedInstanceState) } override fun provideCustomOverlayLayout(): Int? = null override fun onAttach(context: Context) { super.onAttach(context) setPetnowCameraDetectionListener(this) // [!code highlight] // Deprecated in 1.4 — prefer setPetnowCameraDetectionListenerV2 (same result models as iOS) } override fun onDetectionStatus(primaryDetectionStatus: PetnowDetectionStatus) { /* Step 4 */ } override fun onDetectionProgress(progress: Int) { /* Step 4 */ } override fun onDetectionFinished(result: DetectionCaptureResult) { /* Step 3 */ } } ``` Instead of arguments (`ARG_API_KEY`), the license can also be provided by **overriding `provideLicense()`**. ```kotlin override fun provideLicense(): LicenseInfo = LicenseInfo(apiKey = "YOUR_API_KEY") ``` ### Argument Keys [#argument-keys] | Key | Type | Description | | ----------------------------- | ------------ | ----------------------------------------------------- | | `ARG_CAPTURE_SESSION_ID` | String | Capture session ID issued by the server (UUID string) | | `ARG_API_KEY` | String | License API key | | `ARG_DETECTION_CONFIGURATION` | Serializable | `DetectionConfiguration` | *** ## Step 2: Display the Camera Screen [#step-2-display-the-camera-screen] ```kotlin val captureSessionId: UUID = // captureSessionId received from the server val fragment = ClientCameraFragment.newInstance(captureSessionId) supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, fragment) .commit() ``` Once the Fragment is attached, it internally requests the camera permission, initializes the camera, and then starts detection. (Unlike the `CameraView` approach, the Fragment requests permissions **automatically**.) *** ## Step 3: Handle Capture Results [#step-3-handle-capture-results] ```kotlin override fun onDetectionFinished(result: DetectionCaptureResult) { when (result) { is DetectionCaptureResult.Success -> { // result.noseImageFiles: nose print images, result.faceImageFiles: face images uploadImages(result.noseImageFiles, result.faceImageFiles) } is DetectionCaptureResult.Fail -> { showRetryOrExitDialog() } } } ``` ### The DetectionCaptureResult Type [#the-detectioncaptureresult-type] ```kotlin sealed class DetectionCaptureResult { data class Success( val noseImageFiles: List, // biometric set: dog = nose images, cat = empty val faceImageFiles: List // appearance set: face images (for a cat this is the biometric set) ) : DetectionCaptureResult() data object Fail : DetectionCaptureResult() } ``` Even after receiving `Fail`, the server's capture session is still open. Retry (`startDetection()`) or close the screen. If you close it, the server automatically ends (ABORTED) the session after about 5 minutes. *** ## Step 4: Observe Progress and Status [#step-4-observe-progress-and-status] ```kotlin override fun onDetectionProgress(progress: Int) { progressBar.progress = progress // 0~100 } override fun onDetectionStatus(primaryDetectionStatus: PetnowDetectionStatus) { statusTextView.text = when (primaryDetectionStatus) { PetnowDetectionStatus.Detected -> "Detection succeeded" PetnowDetectionStatus.NoObject -> "Fit your pet within the frame" PetnowDetectionStatus.TooClose -> "Move a little farther away" PetnowDetectionStatus.TooFarAway -> "Move a little closer" PetnowDetectionStatus.TooDark -> "Move to a brighter spot" else -> "" } } ``` The V1 listener of `PetnowCameraFragment` uses the `PetnowDetectionStatus` enum (a flat enum). It differs from the `DetectionStatus` (sealed class) used by the V2 listener of `CameraController`. | Category | Values | | --------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Success/no detection | `Detected`, `NoObject` | | Distance | `TooClose`, `TooFarAway` | | Lighting | `TooBright`, `TooDark`, `ShadowDetected`, `GlareDetected` | | Sharpness | `TooBlurred`, `MotionBlurDetected`, `DefocusedBlurDetected` | | Angle | `NotFrontFace`, `NotFrontNose`, `NotFrontNoseTop`, `NotFrontCatFaceHor`, `NotFrontCatFaceTop`, `NotFrontCatFaceBottom` | | Occlusion/obstruction | `NoseNotFound`, `FurDetected` | | Tampering/spoofing | `FakeDetected` | | General | `Error` | *** ## Detection Control [#detection-control] ```kotlin startDetection() // reset progress to 0 and start a new session (re-capture). Returns a Result pauseDetection() // pause detection (returns nothing) resumeDetection() // resume from where it was paused (returns nothing) switchCamera() // switch front/back (returns a Result) ``` In `PetnowCameraFragment`, `resumeDetection()` returns nothing (void). For re-capture (resetting progress), use `startDetection()` and handle success/failure with the returned `Result`. ```kotlin // Example: retrying after a capture failure startDetection() .onSuccess { /* restarted */ } .onFailure { e -> navigateBack() } ``` *** ## Permission Handling [#permission-handling] The Fragment requests the camera permission automatically, but you still need to declare the permission in `AndroidManifest.xml`. ```xml ``` *** ## UI Customization [#ui-customization] `PetnowCameraFragment` can inject a **custom overlay** and a **floating guide** on top of the default tracking UI. ### Adding a Custom Overlay [#adding-a-custom-overlay] Create an XML layout, inject it via `provideCustomOverlayLayout()`, and once inflation is complete, manipulate the views in `onAddedCustomLayout(view)`. ```xml ``` ```kotlin class ClientCameraFragment : PetnowCameraFragment(), PetnowCameraDetectionListener { private var _binding: FragmentClientCameraBinding? = null private val binding get() = _binding!! override fun provideCustomOverlayLayout(): Int = R.layout.fragment_client_camera // [!code highlight] override fun onAddedCustomLayout(view: View) { // [!code highlight] super.onAddedCustomLayout(view) _binding = FragmentClientCameraBinding.bind(view) // bind the inflated view here binding.statusText.text = "Camera is ready" } override fun onDestroyView() { super.onDestroyView() _binding = null } override fun onDetectionProgress(progress: Int) { binding.detectionProgress.progress = progress } override fun onDetectionStatus(primaryDetectionStatus: PetnowDetectionStatus) { binding.statusText.text = when (primaryDetectionStatus) { PetnowDetectionStatus.Detected -> "Nose print scans are coming through!" PetnowDetectionStatus.TooClose -> "It's too close! Move back a bit." PetnowDetectionStatus.TooFarAway -> "It's too far! Move closer." PetnowDetectionStatus.TooDark -> "It's too dark! Try in a brighter place." else -> "Align your pet's face in the frame" } } } ``` | Method | Description | | ------------------------------------ | ----------------------------------------------------------------- | | `provideCustomOverlayLayout(): Int?` | Custom overlay layout resource ID (`null` if none) | | `onAddedCustomLayout(view)` | Called after the overlay is inflated — bind/manipulate views here | Custom overlay UI ### Floating Guide Over the Tracker [#floating-guide-over-the-tracker] Inject a floating guide that follows the tracking UI. The module handles the coordinates and rendering automatically; the app only specifies which view to draw. (Optional) ```kotlin class ClientCameraFragment : PetnowCameraFragment(), PetnowCameraDetectionListener { private var _floatingBinding: LayoutFloatingGuideBinding? = null override fun provideFloatingGuideLayout(): Int = R.layout.layout_floating_guide // [!code highlight] override fun onFloatingGuideAdded(view: View) { // [!code highlight] _floatingBinding = LayoutFloatingGuideBinding.bind(view) _floatingBinding?.layoutCameraGuideFloating?.isVisible = false // hidden because it's shown immediately on inflate _floatingBinding?.textCameraGuideFloatingMessage?.text = "Floating guide text!" } override fun onDetectionProgress(progress: Int) { if (progress > 0) { _floatingBinding?.layoutCameraGuideFloating?.isVisible = true } } } ``` | Method | Description | | ------------------------------------ | -------------------------------------------------- | | `provideFloatingGuideLayout(): Int?` | Floating guide layout resource ID (`null` if none) | | `onFloatingGuideAdded(view)` | Called after the floating guide is inflated |
Floating guide overlay Floating guide tracker
*** ## Next Steps [#next-steps] * [Basic Usage](/petify/docs/sdk/android/basic-usage) - Migrate to the recommended approach (`CameraView` + `CameraController`) * [Fully Custom UI](/petify/docs/sdk/android/custom-ui) - When you need a 100% custom UI * [Sound Guide](/petify/docs/sdk/android/sound) - Configuring sound playback # Getting Started Source: /petify/docs/sdk/android/getting-started > Guide to installing and performing initial setup of the Petnow Android SDK. *** ## Overview [#overview] This guide walks you step by step through installing the Petnow Android SDK into your project and performing the initial setup. ## Prerequisites [#prerequisites] Before you begin, make sure you have the following ready: * A project targeting Android API 28 (Android 9.0) or higher * compileSdk 34 or higher * Java 17 or higher * Kotlin 1.9 or higher * A Petnow API key — issue one from the [Petify Console](/petify/docs/petify-console) (or contact [support@petnow.io](mailto:support@petnow.io)) ## Step 1: Install the SDK [#step-1-install-the-sdk] The Petnow Android SDK is distributed through AWS CodeArtifact. ### Install the AWS CLI [#install-the-aws-cli] Install the AWS CLI from the following link: [https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) ### Issue Credentials from the Petify Console [#issue-credentials-from-the-petify-console] Issue your AWS CodeArtifact credentials from My Page in the [Petify Console](/petify/docs/petify-console), and set the environment variables using the provided values. (See the [Petify Console](/petify/docs/petify-console) doc for sign-up, payment-method registration, and key issuance.) Petify Console – AWS CodeArtifact credentials screen ```bash export AWS_ACCESS_KEY_ID= export AWS_SECRET_ACCESS_KEY= export AWS_DEFAULT_REGION= export PETNOW_CODEARTIFACT_DOMAIN= export PETNOW_AWS_ACCOUNT_ID= export PETNOW_ANDROID_SDK_REPOSITORY= ``` Instead of setting environment variables in every session, you can store your AWS credentials permanently: ```bash $ aws configure AWS Access Key ID [None]: AWS Secret Access Key [None]: Default region name [None]: Default output format [None]: json ``` This saves them to `~/.aws/credentials` and `~/.aws/config`. After configuring, you only need to export the SDK-specific variables: ```bash export PETNOW_CODEARTIFACT_DOMAIN= export PETNOW_AWS_ACCOUNT_ID= export PETNOW_ANDROID_SDK_REPOSITORY= ``` ### Add the Petnow Maven Repository [#add-the-petnow-maven-repository] Derive the repository URL and authentication token from your credentials: ```bash export CODEARTIFACT_URL=$(aws codeartifact get-repository-endpoint \ --domain $PETNOW_CODEARTIFACT_DOMAIN \ --domain-owner $PETNOW_AWS_ACCOUNT_ID \ --repository $PETNOW_ANDROID_SDK_REPOSITORY \ --region $AWS_DEFAULT_REGION \ --format maven \ --query repositoryEndpoint \ --output text) export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token \ --domain $PETNOW_CODEARTIFACT_DOMAIN \ --domain-owner $PETNOW_AWS_ACCOUNT_ID \ --region $AWS_DEFAULT_REGION \ --query authorizationToken \ --output text) ``` > **Token expiration**: Re-run both commands every 12 hours, or whenever a Gradle sync fails with an authentication error. In CI/automated environments, inject the credentials as secrets and refresh the token during the build — see the [AWS CodeArtifact authentication tokens docs](https://docs.aws.amazon.com/codeartifact/latest/ug/tokens-authentication.html). Add the Petnow Maven repository to your project's `settings.gradle.kts` or root `build.gradle.kts` file: ```kotlin dependencyResolutionManagement { repositories { google() mavenCentral() maven { name = "CodeArtifact" url = uri(System.getenv("CODEARTIFACT_URL") ?: "YOUR_REPOSITORY_URL") credentials { username = "aws" password = System.getenv("CODEARTIFACT_AUTH_TOKEN") ?: "YOUR_AUTH_TOKEN" } } } } ``` ### Add the Dependency [#add-the-dependency] Add the following dependency to your app's `build.gradle.kts` file: ```kotlin dependencies { // Petnow SDK implementation("io.petnow:ui:1.5.0") } ``` Using the UI module does not require a separate `api-client` dependency. If you need to call server APIs, handle them directly from your app server. ## Step 2: Configure the Project [#step-2-configure-the-project] ### Configure AndroidManifest.xml [#configure-androidmanifestxml] Add the following permissions and settings to your app's `AndroidManifest.xml` file: ```xml ... ``` | Setting | Description | | ------------------ | ------------------------------------------------------------------------- | | `INTERNET` | Required for API communication and license verification | | `CAMERA` | Required for camera preview and detection | | `largeHeap="true"` | Recommended for optimal performance during image processing and detection | The recommended path, `CameraView` + `CameraController`, does **not** request the camera permission automatically. Declare the `CAMERA` permission above and request the runtime permission yourself before starting a session. (Only the legacy [`PetnowCameraFragment`](/petify/docs/sdk/android/fragment) requests it automatically.) ### Import the Modules [#import-the-modules] ```kotlin import io.petnow.ui.CameraController import io.petnow.ui.CameraView import io.petnow.ui.config.LicenseInfo import io.petnow.callback.PetnowCameraDetectionListenerV2 ``` ## Step 3: Understand License and Detection Configuration [#step-3-understand-license-and-detection-configuration] As of 1.4, there is no global initialization (v1.3.x's `PetnowApiClient.init()`). License and detection configuration are passed **per camera session**. * **License**: Create a `LicenseInfo(apiKey, environment)` and pass it to the `CameraController` constructor. (There is no separate global initialization call.) `environment` is optional. * **Detection configuration**: Create a `DetectionConfiguration` and pass it to `initializeCamera()` when starting the session. ```kotlin import io.petnow.ui.CameraController import io.petnow.ui.config.LicenseInfo // The license is passed when creating the CameraController. val license = LicenseInfo(apiKey = "YOUR_API_KEY") val controller = CameraController( context, // Application/Activity Context license, coroutineScope, // A CoroutineScope valid for the duration of the session ) ``` The API key is validated against the server inside `initializeCamera()` (once per key per process); a rejected key fails with `PetnowUIError.InvalidLicense` and the camera does not open. The same key is also used for monitoring. For key issuance, contact [support@petnow.io](mailto:support@petnow.io). #### Backend Environment (`environment`) [#backend-environment-environment] `LicenseInfo(apiKey: String, environment: PetnowEnvironment? = null)` takes an optional backend selector: ```kotlin import io.petnow.ui.config.LicenseInfo import io.petnow.ui.config.PetnowEnvironment // enum PetnowEnvironment { PRODUCTION, STAGING } val license = LicenseInfo( apiKey = "YOUR_STAGING_API_KEY", environment = PetnowEnvironment.STAGING, ) ``` Omit it to keep the environment the SDK was built for, which is **production** for every published build. Nothing changes for an app that does not set it. A key issued by the staging console is only valid against the staging backend and vice versa — a mismatch fails license validation. `environment` is a named environment on purpose: do **not** wire it to your own `BuildConfig.DEBUG` flag. A release build that accidentally selected staging would send real captures to a backend where the pets are not registered. (This restores the capability v1.3.x had as `isDebugMode`, which was removed in v1.4.) The console can issue several API keys, but one process must use one of them. If `initializeCamera()` is called with a second, **different** API key in the same process, it throws `PetnowUIError.MultipleApiKeys` and the first key keeps working. To run against another key, restart the app. ### Detection Configuration (DetectionConfiguration) [#detection-configuration-detectionconfiguration] ```kotlin import io.petnow.ui.config.DetectionConfiguration import io.petnow.ui.config.DetectionPurpose import io.petnow.ui.config.PetSpecies // For dog profile registration val configuration = DetectionConfiguration( species = PetSpecies.DOG, purpose = DetectionPurpose.PET_PROFILE_REGISTRATION, enableFakeDetection = true, ) ``` This configuration is passed to `controller.initializeCamera(configuration, captureSessionId)` when starting the session. The full flow is covered in [Basic Usage](/petify/docs/sdk/android/basic-usage). ### Parameter Reference [#parameter-reference] | Parameter | Description | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `species` | The pet species. Determines the detection pipeline. | | `purpose` | The capture purpose. The number of required images depends on the purpose. | | `enableFakeDetection` | Spoof detection for photos, screens and printed images. Default `true`. | | `inferenceTier` | Detector execution path (`AUTO` default / `CPU` / `GPU`). Read once per process; see the note below. | | `difficultyMode` | (Deprecated since 1.5.0) Accepted for source compatibility and ignored; nose-image quality is judged by the on-device model. | Spoof detection (photos, screens, printed images) is on by default. Passing `false` turns spoof detection off for that session, which is how you evaluate the SDK against photographs of pets instead of live animals. `inferenceTier` is read **once**, when the process creates its first detector engine, and fixed from then on: a later controller asking for the other tier keeps running on the first decision (the SDK logs a warning). Set it on the first `initializeCamera()` of the process, or restart the app to change it. Leave it at `AUTO` unless support asks you to force a tier. **purpose options:** * `DetectionPurpose.PET_PROFILE_REGISTRATION` - Profile registration * `DetectionPurpose.PET_IDENTIFICATION` - Identification * `DetectionPurpose.PET_VERIFICATION` - Verification **species options:** * `PetSpecies.DOG` - Dog (nose detection) * `PetSpecies.CAT` - Cat (face detection) ### Create a Capture Session [#create-a-capture-session] **Required**: Before starting a camera session, you must create a capture session on your server and obtain a `captureSessionId`. ```kotlin import java.util.UUID lifecycleScope.launch { // Obtain the captureSessionId from your server // (your server calls createCaptureSession of the Petnow Server API) val captureSessionId: UUID = yourServerApi.createCaptureSession( species = "DOG", purpose = "PET_PROFILE_REGISTRATION" ) // Pass the captureSessionId when navigating to the camera screen navigateToCameraScreen(captureSessionId) } ``` **Server API**: The `captureSessionId` is created on your server through the [Petnow Server API](/petify/docs/server/api/overview). It is not created directly on the client. Creating the capture session is **your app server's responsibility** (the client only uses the issued `captureSessionId`). The `petId` requirement depends on the purpose (required for registration/verification, not required for identification). For the creation parameters, the `petId` requirement, and the full server flow through result-image upload, see [Server API – Biometric](/petify/docs/server/api/biometric). For details on how to pass the `captureSessionId`, see the [UI Module Overview](/petify/docs/sdk/android/overview#how-to-pass-capturesessionid). ## Step 4: Verify the Installation [#step-4-verify-the-installation] To verify that the SDK is installed correctly, build and run your app. If the dependencies were resolved successfully, the following code compiles and lets you check whether the device supports the camera. ```kotlin import io.petnow.ui.camera.cameraInfo // Check whether the device supports the camera (there is no global initialization call) val cameraInfo = context.cameraInfo Log.d("PetnowSDK", "supported=${cameraInfo.isSupportedDevice}, front=${cameraInfo.isFrontCameraSupported}") ``` ## Troubleshooting [#troubleshooting] ### Package Not Found [#package-not-found] **Symptom**: Gradle sync fails, dependency not found **Solution:** 1. **Verify CodeArtifact authentication** ```bash # Check the environment variables echo $CODEARTIFACT_URL echo $CODEARTIFACT_AUTH_TOKEN ``` 2. **Refresh the token** * The CodeArtifact token expires after 12 hours * Re-issue the token with the following command: ```bash export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token \ --domain $PETNOW_CODEARTIFACT_DOMAIN \ --domain-owner $PETNOW_AWS_ACCOUNT_ID \ --region $AWS_DEFAULT_REGION \ --query authorizationToken \ --output text) ``` 3. **Clear the Gradle cache** ```bash ./gradlew clean ./gradlew --refresh-dependencies ``` ### API Key [#api-key] **Symptom**: `initializeCamera()` fails with `PetnowUIError.InvalidLicense` and the camera does not open **Solution:** * A rejected key surfaces as `PetnowUIError.InvalidLicense` from `initializeCamera()` — the camera does not open with a bad key. * Verify that the key passed to `LicenseInfo(apiKey = ...)` is correct * Check that the key and the `environment` match: a staging-console key is only valid against the staging backend, and a production key only against production. * A second, different API key in the same process fails with `PetnowUIError.MultipleApiKeys`; the first key keeps working, so use one key per process and restart the app to switch. * Contact [support@petnow.io](mailto:support@petnow.io) ### Camera Permission Error [#camera-permission-error] **Symptom**: The camera does not appear, or the permission request fails **Solution:** * Verify that the `CAMERA` permission is declared in `AndroidManifest.xml` * On the `CameraView` + `CameraController` path, the host must request the runtime camera permission before starting a session (only the legacy `PetnowCameraFragment` requests it automatically) * If the user denied the permission, guide them to grant it manually in Settings ### OutOfMemoryError [#outofmemoryerror] **Symptom**: Out-of-memory error during image processing **Solution:** * Verify that `android:largeHeap="true"` is added to `AndroidManifest.xml` * Clear unnecessary image caches ## API Key Security [#api-key-security] The key you pass to `LicenseInfo(apiKey = ...)` is the **same Petnow API key type** used for `x-petnow-api-key` on the [Server API](/petify/docs/server/api/overview). Treat it as a credential: anything shipped inside an app binary can be read back out of it. **Recommended setup** * Issue a **dedicated key for the mobile app**, separate from the key your server uses. An account can hold multiple active keys. * Do not embed the server key in the app. Where possible, fetch the app key from your own server at runtime instead of hardcoding it. **Revocation** * Keys are deactivated in the [Petify Console](/petify/docs/petify-console) (My Page → API Key Management). * Deactivation takes effect immediately and does not affect other keys. * A deactivated key cannot be reactivated — create a new one instead. **Zero-downtime rotation on an app release** 1. Create a new app key. 2. Ship the new app version with the new key. 3. Let older app versions drain. 4. Deactivate the old key. There is no automatic key rotation inside the SDK. The CodeArtifact / AWS access keys from Step 1 are used only to download the SDK at build time. They are unrelated to runtime authentication. ## Next Steps [#next-steps] Installation and setup are complete! You're now ready to use the SDK. * [UI Module Overview](/petify/docs/sdk/android/overview) - Understand the UI module structure * [Basic Usage](/petify/docs/sdk/android/basic-usage) - Integrate the camera UI ## Support [#support] If you run into any issues during installation, contact [support@petnow.io](mailto:support@petnow.io). # Android SDK Introduction Source: /petify/docs/sdk/android/intro > Introduction to the technical requirements and features of the Petnow Android SDK. ## Latest Version [#latest-version] | Module | Version | | --------- | ------- | | UI Module | 1.5.0 | **Server API Recommended**: Pet management operations such as registration, verification, and identification should be performed on your server using the [Server API](/petify/docs/server/api/overview). The client is responsible only for capturing images using the UI module. *** ## Android SDK Features [#android-sdk-features] The Petnow Android SDK is a native library written in Kotlin, compatible with both Kotlin and Java. ## SDK Architecture [#sdk-architecture] ### Key Components [#key-components] **UI Module** * `CameraController` * The core engine that manages the entire lifecycle of the camera and detection session. * Created with a license (`LicenseInfo`) and controlled via `initializeCamera()` / `startDetection()` and similar methods. * Detection events are delivered through `PetnowCameraDetectionListenerV2`, and state through the `state` property (StateFlow). * `CameraView` * A `FrameLayout` widget that renders the camera preview and the default tracking UI. * Connect it to a controller with `cameraView.controller = controller`. * `PetnowCameraFragment` *(legacy)* * A Fragment-hosting approach. For new integrations, we recommend `CameraView` + `CameraController`. See [Fragment Approach (legacy)](/petify/docs/sdk/android/fragment). ## Technical Requirements [#technical-requirements] * **Minimum Android Version**: API 28 (Android 9.0) or higher * **ABIs**: `arm64-v8a` (devices) and `x86_64` (emulators, since 1.5.0). `armeabi-v7a` is not shipped * **compileSdk / targetSdk**: 34 * **Java**: 17 or higher * **Kotlin**: 1.9 or higher ## Next Steps [#next-steps] To integrate the SDK into your Android app, refer to the following documentation: * [Getting Started](/petify/docs/sdk/android/getting-started) - Installation and initial setup * [UI Module](/petify/docs/sdk/android/overview) - Camera integration # UI Module Overview Source: /petify/docs/sdk/android/overview > An explanation of the structure and core components of the Petnow Android UI module. *** ## What is the Petnow UI Module? [#what-is-the-petnow-ui-module] The Petnow UI module is a module that provides a **ready-to-use camera engine** for capturing pet biometric data. It handles all the complex camera setup, detection logic, and image processing, so developers can integrate professional biometric recognition features into their apps with just a few lines of code. UI Module Intro ### Core Values [#core-values] * 🚀 **Fast integration**: No need to understand the complex Camera2 API * 🎯 **Real-time guidance**: Guides users on the optimal way to capture * 🎨 **Customizable**: Adjustable to match your app's design * 🧩 **Flexible hosting**: Integrate anywhere — View, Jetpack Compose, React Native, and more ## Core Components [#core-components] The UI module consists of **a single engine (`CameraController`)** and **multiple hosting methods** that place it on the screen. ### CameraController (Engine) [#cameracontroller-engine] The core component that manages the entire lifecycle of the camera/detection session. It is not involved in UI rendering; it only forwards state and events to the outside. ```kotlin import io.petnow.ui.CameraController import io.petnow.ui.config.LicenseInfo val controller = CameraController( context, LicenseInfo(apiKey = "YOUR_API_KEY"), coroutineScope, ) ``` **Responsibilities:** * Opening/closing the camera, starting/pausing/resuming the detection session * Providing the detection state via the `PetnowCameraDetectionListenerV2` callback and the `state` property (StateFlow) * Delivering the final result (`CameraResult`) via a callback ### Choosing a Hosting Method [#choosing-a-hosting-method] | Method | Description | Documentation | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | **`CameraView`** (recommended) | A `FrameLayout` widget that draws the preview + default tracking UI. Connect it with `cameraView.controller = controller` | [Basic Usage](/petify/docs/sdk/android/basic-usage) | | **Fully custom** | Connect your own `SurfaceView` with `attachPreviewSurface()`. The SDK does not draw any UI (100% custom) | [Fully Custom UI](/petify/docs/sdk/android/custom-ui) | | **`PetnowCameraFragment`** *(legacy)* | A Fragment-inheritance approach. Not recommended for new integrations | [Fragment Method (Legacy)](/petify/docs/sdk/android/fragment) | ## Architecture [#architecture] The Petnow UI module hides the complex state management and ML framework integration logic, providing developers with only a simple interface. ## Session Concepts [#session-concepts] The capture process is managed by two types of sessions: **Capture Session** * Created by calling the Petnow Server API (`createCaptureSession`) from your server * The `captureSessionId` received from the server is passed to the client and supplied to `initializeCamera()` * One Capture Session can contain multiple Detection Sessions **Detection Session** * A session from when actual detection starts until detection completes or fails * When the user retakes (calls `startDetection()` again), a new Detection Session starts ### How to Pass captureSessionId [#how-to-pass-capturesessionid] Pass the `captureSessionId` (UUID) received from the server to `initializeCamera()`. ```kotlin import java.util.UUID lifecycleScope.launch { // Obtain the captureSessionId from your server // (the server calls createCaptureSession of the Petnow Server API) val captureSessionId: UUID = yourServerApi.createCaptureSession( species = "DOG", purpose = "PET_PROFILE_REGISTRATION", ) controller.initializeCamera(configuration, captureSessionId) controller.startDetection() } ``` **Server API**: The `captureSessionId` is created on the server through the [Petnow Server API](/petify/docs/server/api/overview). It is not created directly on the client. ## State Management [#state-management] ### DetectionStatus [#detectionstatus] The representative detection status of the current frame, delivered via `PetnowCameraDetectionListenerV2.onDetectionStatus`. It is a sealed class, and `Failed` includes a specific cause (`reason`). ```kotlin import io.petnow.ui.status.DetectionStatus import io.petnow.ui.status.DetectionFailureReason sealed class DetectionStatus { data object NoObject : DetectionStatus() // No target detected data object Processing : DetectionStatus() // Detection in progress data object Detected : DetectionStatus() // Detection succeeded data object Finished : DetectionStatus() // Capture complete data class Failed(val reason: DetectionFailureReason) : DetectionStatus() } ``` For the full list of `DetectionFailureReason` values, see [Basic Usage](/petify/docs/sdk/android/basic-usage#step-5-observe-status-and-progress). ### CameraResult [#cameraresult] The final capture result. ```kotlin import io.petnow.ui.CameraResult sealed class CameraResult { data class Success( val fingerprintImageFiles: List, // For biometric recognition (dog = nose, cat = face) val appearanceImageFiles: List // Appearance (face) images ) : CameraResult() data object Fail : CameraResult() } ``` ## Data Flow [#data-flow] > **Key point**: Once you create a `CameraController` and register a `PetnowCameraDetectionListenerV2`, the controller handles all camera logic and delivers only the necessary events via callbacks. ### Data Flow Description [#data-flow-description] **1. Real-time status updates** * You receive the following events through `PetnowCameraDetectionListenerV2`: * `onDetectionStatus(status)`: The detection status of the current frame (`DetectionStatus`) * `onDetectionProgress(progress)`: Detection progress (0 \~ 100) * If you need nose/face tracking boxes, use `state.value.currentDetectionResult` (`DetectionResult`). **2. Final result delivery** * When detection completes, the controller saves the images and creates a `CameraResult`. * The result is delivered via the `onDetectionFinished(result)` callback. * `CameraResult.Success` - Capture successful (includes the image file list) * `CameraResult.Fail` - Capture failed ## Next Steps [#next-steps] Now that you understand the structure of the UI module, refer to the following documentation: * [Basic Usage](/petify/docs/sdk/android/basic-usage) - Integrating `CameraView` + `CameraController` * [Fully Custom UI](/petify/docs/sdk/android/custom-ui) - A 100% custom UI with your own SurfaceView * [Customization](/petify/docs/sdk/android/customization) - Customizing the `CameraView` guide UI * [Sound Guide](/petify/docs/sdk/android/sound) - Detailed sound playback guide * [Fragment Method (Legacy)](/petify/docs/sdk/android/fragment) - The existing `PetnowCameraFragment` integration method ## Reference [#reference] * [Getting Started](/petify/docs/sdk/android/getting-started) - SDK installation # Sound Guide Source: /petify/docs/sdk/android/sound > Playing pet attention-grabbing sounds with SoundPlayer. *** Before starting this guide, complete [Basic Usage](/petify/docs/sdk/android/basic-usage). ## Overview [#overview] The UI module includes 6 built-in sounds to attract pets' attention. `SoundPlayer` in `io.petnow.ui.sound` plays them, and it is independent of the camera — you can use it on a guide screen before capture starts, not only during capture. `CameraController.playSound()` / `stopSound()` still work and are kept for source compatibility, but they raise a deprecation warning. Use `SoundPlayer` instead. ## Available Sounds [#available-sounds] ### Complete Sound List [#complete-sound-list] **Sound Preview** Click each sound to preview it. Test your pet's reaction! The preview names below are sample audio file names; in code, use the constant in the `SoundType` column. | Sound | `SoundType` | Description | Target | Preview | | -------------- | ---------------------- | ----------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- | | 🥔 bagOfChips | `BAG_OF_CHIPS` | Chip bag rustling sound | 🐕🐱 Both | | | 🥫 openingCan | `OPENING_CAN` | Can opening sound | 🐕🐱 Both | | | 🍚 pouringFood | `POURING_FOOD` | Food pouring sound | 🐕🐱 Both | | | 🥣 shakeFood | `SHAKERS` | Food shaking sound | 🐕🐱 Both | | | 🧸 squeakyToy | `DOG_ONLY_SQUEAKY_TOY` | Squeaky toy sound | 🐕 Dogs | | | 😽 pspspsSound | `CAT_ONLY_PSPSPS` | Pspsps sound | 🐱 Cats | | ## Basic Usage [#basic-usage] ### 1. Create and prepare the player [#1-create-and-prepare-the-player] `prepare()` loads the samples and is a `suspend` function, so call it from a coroutine. Until it finishes, `play()` does nothing. ```kotlin import io.petnow.ui.sound.SoundPlayer import io.petnow.ui.sound.SoundType private val sounds = SoundPlayer(context) private var soundHandle: Int = -1 lifecycleScope.launch { sounds.prepare() } ``` ### 2. Play a Random Sound [#2-play-a-random-sound] Plays one of the loaded sounds at random. `RANDOM` does not take the species into account on Android, so pass a specific `SoundType` when the sound has to match the species. ```kotlin soundHandle = sounds.play(SoundType.RANDOM) ``` `play()` returns a handle for stopping the sound, or `-1` when the player is not prepared yet or no sample is mapped. ### 3. Play a Specific Sound [#3-play-a-specific-sound] Directly select and play the sound you want. ```kotlin enum class SoundType { RANDOM, POURING_FOOD, BAG_OF_CHIPS, OPENING_CAN, DOG_ONLY_SQUEAKY_TOY, CAT_ONLY_PSPSPS, SHAKERS } ``` ```kotlin // Can opening sound soundHandle = sounds.play(SoundType.OPENING_CAN) ``` ### 4. Stop a Sound [#4-stop-a-sound] ```kotlin sounds.stop(soundHandle) ``` ### 5. Release the player [#5-release-the-player] Free the native resources when the screen goes away. The instance cannot be used afterwards, so create a new one if you need sound again. ```kotlin override fun onDestroy() { sounds.release() super.onDestroy() } ``` # Android Changelog Source: /petify/docs/sdk/changelog/android > Per-version changes for the Android SDK This page contains the full release history of the Android SDK. For the latest release, see the [Changelog main page](/petify/docs/sdk/changelog). *** ## \[1.5.0] - 2026-09-28 [#150---2026-09-28] ### UI Module v1.5.0 [#ui-module-v150] **Rebuild the app; do not swap the AAR.** 1.5 is not binary-compatible with 1.4.5. → [v1.4 → v1.5 Migration](/petify/docs/sdk/migration/sdk-v1.4-to-v1.5) #### ✨ Added [#-added] * `LicenseInfo.environment` — choose the backend the key authenticates against. * `DetectionConfiguration.inferenceTier` — choose the detector path (`AUTO` by default). * `CameraController.performanceStats` and `PetnowCameraFragment.performanceStats()` — camera and detector figures. * `io.petnow.ui.sound.SoundPlayer` — guidance sounds without a camera controller. → [Sound Guide](/petify/docs/sdk/android/sound) #### 🔧 Changed [#-changed] * `enableFakeDetection` now defaults to **`true`**. #### 🐛 Fixed [#-fixed] * A second, different API key is refused (`PetnowUIError.MultipleApiKeys`). #### ⚠️ Deprecated [#️-deprecated] * `DetectionConfiguration.difficultyMode` — accepted and ignored. * `CameraController.playSound()` / `stopSound()` — use `SoundPlayer`. Android 1.4.1–1.4.4 were React Native-only releases. Upgrade from 1.4.0 or 1.4.5 directly to 1.5.0. *** ## \[1.4.5] - 2026-09-03 [#145---2026-09-03] ### UI Module v1.4.5 [#ui-module-v145] * Internal improvements and stability fixes. *** ## \[1.4.0] - 2026-06-08 [#140---2026-06-08] ### UI Module v1.4.0 [#ui-module-v140] #### ✨ Added [#-added-1] * Server-side license validation in `initializeCamera()` — a rejected key fails with `PetnowUIError.InvalidLicense`. #### 💥 Changed (breaking) [#-changed-breaking] * Initialization failures are thrown as `PetnowUIError` (`InvalidLicense` / `PermissionDenied` / `CameraOpenFailed`); `switchCamera()` failures as `PetnowCaptureException`. → [v1.3 → v1.4 migration](/petify/docs/sdk/migration/sdk-v1.3-to-v1.4) #### 🔧 Changed [#-changed-1] * R8/ProGuard release builds keep the public `config` and `sound` types. *** ## \[1.3.2] - 2026-04-28 [#132---2026-04-28] ### UI Module v1.3.2 [#ui-module-v132] #### 🐛 Fixed [#-fixed-1] * **Fixed camera teardown crash**: Resolved an issue where the app intermittently crashed when closing the camera * Stability improvements *** ## \[1.3.1] - 2026-04-09 [#131---2026-04-09] ### UI Module v1.3.1 [#ui-module-v131] #### 🐛 Fixed [#-fixed-2] * **Fixed camera preview display error**: Resolved an issue where the preview was not displayed at the correct aspect ratio when entering the camera *** ## \[1.3.0] - 2026-01-29 [#130---2026-01-29] ### UI Module v1.3.0 [#ui-module-v130] #### ✨ Added [#-added-2] * **Overlay customization**: Custom UI can be placed on top of the detection area * **Sound control**: `playSound`, `stopSound` API * **Capture progress tracking**: Provides real-time detection state and detection area coordinates #### 🐛 Fixed [#-fixed-3] * Improved preservation of settings when switching cameras * Stability improvements *** ## \[1.2.4] - 2025-10-29 [#124---2025-10-29] ### UI Module v1.2.4 [#ui-module-v124] #### 🐛 Fixed [#-fixed-4] * Improved event tracking accuracy *** ## \[1.2.3] - 2025-09-23 [#123---2025-09-23] ### UI Module v1.2.3 [#ui-module-v123] #### ✨ Added [#-added-3] * Support for the 16 KB page size policy applied from Android 15 *** ## \[1.2.2] - 2025-07-29 [#122---2025-07-29] ### UI Module v1.2.2 [#ui-module-v122] #### 🐛 Fixed [#-fixed-5] * Stability and performance improvements *** ## \[1.2.1] - 2025-07-29 [#121---2025-07-29] ### UI Module v1.2.1 [#ui-module-v121] #### 🐛 Fixed [#-fixed-6] * Stability improvements *** ## \[1.2.0] - 2025-07-29 [#120---2025-07-29] ### UI Module v1.2.0 [#ui-module-v120] Initial public release *** ## \[1.0.1] - 2025-06-05 [#101---2025-06-05] ### UI Module v1.0.1 [#ui-module-v101] #### 🐛 Fixed [#-fixed-7] * Stability improvements *** ## \[1.0.0] - 2025-06-05 [#100---2025-06-05] ### UI Module v1.0.0 [#ui-module-v100] #### ✨ Added [#-added-4] * Initial release * Fragment-based camera detection feature * Provides PetnowCameraFragment # Latest Version Source: /petify/docs/sdk/changelog > Latest version changes for the SDK The iOS and Android SDKs are currently versioned separately (the React Native package is aligned with the native UI module version). They will move to a unified version in the future. ## Latest release: v1.5.0 [#latest-release-v150] ### 📦 Version Info [#-version-info] | Module | iOS | Android | | ------------- | ------ | ------- | | **UI Module** | v1.5.0 | v1.5.0 | **React Native** (`@petify/react-native-camera-ui`): v1.5.0 **Release date**: 2026-09-28 *** ## Highlights (v1.5.0) [#highlights-v150] 1.5 is not binary-compatible on Android. An app compiled against 1.4.5 whose AAR is replaced with 1.5, without being rebuilt, starts normally and then throws `NoSuchMethodError` the first time it constructs a configuration. Rebuild against 1.5 — there is nothing to change in the source. → [v1.4 → v1.5 Migration](/petify/docs/sdk/migration/sdk-v1.4-to-v1.5) ### ✨ Common [#-common] * **`enableFakeDetection` now defaults to `true`.** Spoof (photo, screen, print) detection is on unless you pass `false`, which was the 1.4.5 behavior in reverse. Review your call sites: a flow tested against photographs of pets will start failing. * **`LicenseInfo.environment` picks the backend.** `production` (default) or `staging`, so a staging-console key can be used against staging. Restores what v1.3.x had as `isDebugMode`. * **One API key per process.** A second, different key is refused instead of silently replacing the first (`PetnowUIError.MultipleApiKeys` / `.multipleApiKeys`). * **`difficultyMode` is deprecated and ignored** (iOS/Android `DetectionConfiguration`, React Native ``). Existing code still compiles; it goes in the next major release. * **Performance figures for diagnostics.** `CameraController` exposes `performanceStats` (camera init time, first-frame time, processed-frame fps, detector ms) — a `StateFlow` on Android and a `@Published` property on iOS. ### 🤖 Android [#-android] * **`DetectionConfiguration.inferenceTier`** (`AUTO` / `CPU` / `GPU`, default `AUTO`) selects the detector path. `AUTO` picks the path that suits the device and falls back if that path is unavailable. It is read once, when the process creates its first detector engine, and fixed from then on. The chosen tier is reported in `CameraPerformanceStats.inferenceTier`. * `LicenseInfo(apiKey, environment)` — `PetnowEnvironment.PRODUCTION` / `.STAGING`. * **`io.petnow.ui.sound.SoundPlayer`** plays the guidance sounds without a camera controller. `CameraController.playSound()` / `stopSound()` are deprecated. → [Sound Guide](/petify/docs/sdk/android/sound) ### 🍎 iOS [#-ios] * `CameraController.performanceStats` (`CameraPerformanceStats`, `@Published`). * `LicenseInfo(apiKey:environment:)` — `.production` / `.staging`. * `CameraOpticsPolicy` and the adaptive rear-camera mode are removed. Rear lens selection behaves as in 1.4.3. ### ⚛️ React Native [#️-react-native] * The `onRealtimeServerIqaDiagnostic` prop and the `RealtimeServerIqa*` types are removed. Nothing replaces them. * `usePetnowCamera({ apiKey, environment })` — `'production'` (default) or `'staging'`. * `difficultyMode` prop is deprecated and never reaches native. *** ## Highlights (v1.4.5) [#highlights-v145] The first release that ships the iOS/Android UI modules and the React Native package together since v1.4.0. ### ⚛️ React Native [#️-react-native-1] * Fixes the Android app build on RN 0.82+, keeping the full **0.75 – 0.86.x** range buildable. * Carries React Native 0.86 / React 19 support (shipped as 1.4.4). * Also includes the `showDetectionMarker` prop (1.4.3). *** ## Highlights (v1.4.0) [#highlights-v140] A release focused on internal structure improvements and stabilization. ### ✨ Common [#-common-1] * Detection behaviors such as pause/resume, session handling, and difficulty mode now work identically on iOS and Android. * License validation and initialization error handling are now aligned across platforms — Android also validates the API key server-side in `initializeCamera()`, and throws initialization failures as a `PetnowUIError`, matching the iOS error model. → [Android Changelog](/petify/docs/sdk/changelog/android) ### ⚛️ React Native [#️-react-native-2] * First release of `@petify/react-native-camera-ui` v1.4.0 — a camera UI package built on the New Architecture (Fabric/TurboModules). It wraps the iOS/Android UI modules v1.4.0. *** ## Highlights (v1.3.2) [#highlights-v132] ### 🐛 Android Bug Fixes [#-android-bug-fixes] * **Fixed camera teardown crash**: Resolved an issue where the app intermittently crashed when closing the camera * Stability improvements ### 🍎 iOS [#-ios-1] * No code changes (version sync) *** ## Highlights (v1.3.1) [#highlights-v131] ### 🐛 Android Bug Fixes [#-android-bug-fixes-1] * **Fixed camera preview display error**: Resolved an issue where the preview was not displayed at the correct aspect ratio when entering the camera ### 🍎 iOS [#-ios-2] * No code changes (version sync) *** ## Highlights [#highlights] ### ✨ Common Improvements [#-common-improvements] #### V2 API Integration [#v2-api-integration] * Both iOS and Android updated to the server API V2 spec * Switched to a session-based workflow #### UI/UX Improvements [#uiux-improvements] * Enhanced customization capabilities * Added sound playback feature *** ### 🍎 iOS Highlights [#-ios-highlights] #### UI Module v1.3.0 (v1.2.2 → v1.3.0) [#ui-module-v130-v122--v130] ##### ✨ Added [#-added] * **Overlay customization**: Place custom UI on top of the detection area * **Sound control**: `playSound`, `stopSound` API * **Detection state tracking**: Provides progress (`currentCount`) and detection area coordinates (`detectedObjectNormalizedRect`) ##### 🔧 Changed [#-changed] * **Improved initialization flow**: Added the `isInitialized` property and guaranteed waiting for `initializeCamera()` to complete (the completion of initializeCamera() now marks the completion of initialization.) * **AVCaptureSession external injection support**: In cross-platform environments, you can configure the Preview first and then inject the Session * **Reduced fake detection false positives**: Relaxed thresholds * **Added failure reasons**: Added new status values to the `DetectionFailureReason` enum ##### 🧹 Cleanup [#-cleanup] * Removed unused properties For details, see the [iOS Changelog](/petify/docs/sdk/changelog/ios). *** ### 🤖 Android Highlights [#-android-highlights] #### UI Module v1.3.0 (v1.2.4 → v1.3.0) [#ui-module-v130-v124--v130] ##### ✨ Added [#-added-1] * **Overlay customization**: Place custom UI on top of the detection area with `provideFloatingGuideLayout()` * **Sound control**: `playSound`, `stopSound` API * **Detection state tracking**: Provides real-time detection state and detection area coordinates ##### 🐛 Fixed [#-fixed] * Fixed an issue where settings were not preserved when switching cameras * Fixed an issue where the `onDetectionFinished` callback was called on a background thread * Fixed ObjectDetector errors * **Added failure reasons**: Added new status values to the `DetectionStatus` enum For details, see the [Android Changelog](/petify/docs/sdk/changelog/android). *** ## Migration Guide [#migration-guide] * v1.4.x → v1.5.0: [v1.4 → v1.5 Migration](/petify/docs/sdk/migration/sdk-v1.4-to-v1.5) * v1.3.x → v1.4.0: [v1.3 → v1.4 Migration](/petify/docs/sdk/migration/sdk-v1.3-to-v1.4) *** ## Full History by Platform [#full-history-by-platform] For detailed per-version changes, see the pages below: * **[iOS Changelog](/petify/docs/sdk/changelog/ios)** - Full release history of the iOS SDK * **[Android Changelog](/petify/docs/sdk/changelog/android)** - Full release history of the Android SDK # iOS Changelog Source: /petify/docs/sdk/changelog/ios > Per-version changes for the iOS SDK This page contains the full release history of the iOS SDK. For the latest release, see the [Changelog main page](/petify/docs/sdk/changelog). *** ## \[1.5.0] - 2026-09-28 [#150---2026-09-28] ### UI Module v1.5.0 [#ui-module-v150] #### ✨ Added [#-added] * `LicenseInfo(apiKey:environment:)` — choose the backend the key authenticates against. * `CameraController.performanceStats` — camera and detector figures. #### 🔧 Changed [#-changed] * `enableFakeDetection` now defaults to **`true`**. #### 🗑️ Removed (breaking) [#️-removed-breaking] * `CameraOpticsPolicy` and the adaptive rear-camera mode. Rear lens selection behaves as in 1.4.3. #### 🐛 Fixed [#-fixed] * A second, different API key is refused (`PetnowUIError.multipleApiKeys`). #### ⚠️ Deprecated [#️-deprecated] * `DetectionConfiguration.difficultyMode` — accepted and ignored. *** ## \[1.4.5] - 2026-09-03 [#145---2026-09-03] ### UI Module v1.4.5 [#ui-module-v145] * Internal improvements and stability fixes. *** ## \[1.4.0] - 2026-06-08 [#140---2026-06-08] ### UI Module v1.4.0 [#ui-module-v140] #### 🔧 Changed [#-changed-1] * pause/resume preserves the existing detection session. #### ⚠️ Deprecated [#️-deprecated-1] * `CameraViewModel` → `CameraController`, `CameraView(viewModel:)` → `(controller:)`, `stopDetection()` → `finalizeCamera()`, `startDetectionSession()` → `startDetection()`; the license moves to the controller initializer. Old symbols still build as deprecated aliases — see the [v1.3 → v1.4 migration](/petify/docs/sdk/migration/sdk-v1.3-to-v1.4). *** ## \[1.3.2] - 2026-04-28 [#132---2026-04-28] ### UI Module v1.3.2 [#ui-module-v132] * No code changes (version sync following the Android hotfix) *** ## \[1.3.1] - 2026-04-09 [#131---2026-04-09] ### UI Module v1.3.1 [#ui-module-v131] * No code changes (version sync following the Android hotfix) *** ## \[1.3.0] - 2026-01-29 [#130---2026-01-29] ### UI Module v1.3.0 [#ui-module-v130] #### ✨ Added [#-added-1] * **Overlay customization**: Place custom UI on top of the detection area * **Sound control**: `playSound`, `stopSound` API * **Capture progress tracking**: Provides the current capture count (`currentCount`) and detection area coordinates (`detectedObjectNormalizedRect`) #### 🔧 Changed [#-changed-2] * **Improved initialization flow**: Initialization state can be checked via the `isInitialized` property * **Reduced detection errors**: Improved fake detection accuracy * **Detailed failure reasons**: Added more error types to `DetectionFailureReason` #### 🧹 Cleanup [#-cleanup] * API simplification: Consolidated into `requestedImageCount` (removed the existing `requestedMinimumImageCount`) *** ## \[1.2.2] - 2025-08-29 [#122---2025-08-29] ### UI Module v1.2.2 [#ui-module-v122] #### ✨ Added [#-added-2] * **Fake detection control**: Added the `setFakeDetection` API to toggle fake detection #### 🐛 Fixed [#-fixed-1] * Improved camera image size accuracy * Optimized detection accuracy *** ## \[1.2.1] - 2025-08-19 [#121---2025-08-19] ### UI Module v1.2.1 [#ui-module-v121] #### 🐛 Fixed [#-fixed-2] * Stability and performance improvements *** ## \[1.2.0] - 2025-07-29 [#120---2025-07-29] ### UI Module v1.2.0 [#ui-module-v120] Initial public release *** ## \[1.1.0] - 2025-06-05 [#110---2025-06-05] ### UI Module v1.1.0 [#ui-module-v110] Internal test version *** ## \[1.0.1] - 2025-06-05 [#101---2025-06-05] ### UI Module v1.0.1 [#ui-module-v101] #### 🐛 Fixed [#-fixed-3] * Stability improvements *** ## \[1.0.0] - 2025-06-05 [#100---2025-06-05] ### UI Module v1.0.0 [#ui-module-v100] #### ✨ Added [#-added-3] * Initial release * Basic camera detection feature * Provides a SwiftUI-based CameraView # Basic Usage Source: /petify/docs/sdk/ios/basic-usage > A step-by-step guide to integrating CameraView into your app and capturing biometric data. *** ## Before You Start [#before-you-start] Complete [Getting Started](/petify/docs/sdk/ios/getting-started) before following this guide. Issuing a Petnow API key and installing the SPM package are required. This guide walks you step by step through integrating pet nose-print/face capture into your app using `CameraView` and `CameraController`, the core components of the PetnowUI module.
### Create a CameraController [#create-a-cameracontroller] Create the controller with your capture settings (`DetectionConfiguration`) and license (`LicenseInfo`). ### Initialize the Camera [#initialize-the-camera] Connect the capture session with `initializeCamera()`, then start detection with `startDetection()`. ### Handle Capture Results [#handle-capture-results] Handle the success/failure callbacks. ### Handle Errors [#handle-errors] Handle initialization errors correctly. ### Additional Features [#additional-features] Observe progress/status, switch cameras, and re-run detection.
*** ## Step 1: Create a CameraController [#step-1-create-a-cameracontroller] `CameraController` is the core object that manages the camera session and detection logic. You **pass the capture settings and API key to the initializer**, and in SwiftUI you create it as a `@StateObject`. ```swift import SwiftUI import PetnowUI struct PetCameraView: View { @StateObject private var controller: CameraController // [!code highlight] private let captureSessionId: UUID // Session ID received from the server init(captureSessionId: UUID) { self.captureSessionId = captureSessionId _controller = StateObject(wrappedValue: CameraController( // [!code highlight] configuration: DetectionConfiguration( // [!code highlight] species: .dog, // .dog or .cat // [!code highlight] purpose: .petProfileRegistration // Capture purpose // [!code highlight] ), // [!code highlight] licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY") // [!code highlight] )) // [!code highlight] } var body: some View { // Display CameraView in Step 2 CameraView(controller: controller) .task { await initializeCamera() } } } ``` ### Understanding the Parameters [#understanding-the-parameters] **DetectionConfiguration** - Capture settings ```swift public struct DetectionConfiguration { public let species: Species // .dog / .cat public let purpose: DetectionPurpose // Capture purpose public let enableFakeDetection: Bool // Spoof detection, default true // Deprecated since 1.5.0 — always nil. A deprecated init overload still // accepts `difficultyMode:` for source compatibility and discards it. @available(*, deprecated) public var difficultyMode: DifficultyMode? { nil } } ``` Spoof detection (photos, screens, printed images) is on by default. Passing `false` turns spoof detection off for that session, which is how you evaluate the SDK against photographs of pets instead of live animals. **species** - Pet type ```swift public enum Species { case dog // Dog nose-print capture case cat // Cat face capture } ``` **purpose** - Capture purpose (`DetectionPurpose`) ```swift public enum DetectionPurpose { case petProfileRegistration // Profile registration (requires multiple images) case petVerification // Verification case petIdentification // Search/identification } ``` **Differences by Capture Purpose** The number of required images varies depending on the purpose: * `petProfileRegistration`: Requires multiple images * `petVerification`, `petIdentification`: Minimum number of images for fast verification/search **LicenseInfo** - API key and backend ```swift public enum PetnowEnvironment { case production case staging } public struct LicenseInfo { public let apiKey: String public let environment: PetnowEnvironment? // optional public init(apiKey: String, environment: PetnowEnvironment? = nil) } ``` Omit `environment` to keep the environment the SDK was built for, which is **production** for every published build; nothing changes for an app that does not set it. A key issued by the staging console is only valid against the staging backend and vice versa — a mismatch fails license validation. Do **not** wire `environment` to your own `DEBUG` flag: a release build that accidentally selected staging would send real captures to a backend where the pets are not registered. (This restores the capability v1.3.x had as `isDebugMode`, which was removed in v1.4.) *** ## Step 2: Initialize the Camera [#step-2-initialize-the-camera] You must pass the `captureSessionId` issued by the server when initializing the camera. See [Getting Started](/petify/docs/sdk/ios/getting-started) for how to create a session. Call `initializeCamera()` to initialize the camera, then call `startDetection()` to begin detection. Since the license was **already passed in the Step 1 initializer**, you do not pass it here. It is a good idea to display a loading screen until initialization completes. ```swift import SwiftUI import PetnowUI struct CameraScreenView: View { @StateObject private var controller: CameraController private let captureSessionId: UUID init(captureSessionId: UUID) { self.captureSessionId = captureSessionId _controller = StateObject(wrappedValue: CameraController( configuration: DetectionConfiguration(species: .dog, purpose: .petProfileRegistration), licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY") )) } var body: some View { ZStack { // Display a loading screen while initializing if controller.isInitialized { CameraView(controller: controller) } else { ProgressView() // your own loading view (not provided by the SDK) } } .task { await initializeCamera() } } private func initializeCamera() async { do { try await controller.initializeCamera( // [!code highlight] initialPosition: .back, // Use the rear camera // [!code highlight] captureSessionId: captureSessionId // Session ID created on the server // [!code highlight] ) { result in // [!code highlight] // Callback that handles the capture result (implemented in Step 3) print("Capture complete: \(result)") } controller.startDetection() // [!code highlight] // When await returns, isInitialized becomes true and the loading screen disappears } catch { print("Camera initialization failed: \(error.localizedDescription)") } } } ``` ### initializeCamera Parameters [#initializecamera-parameters] **initialPosition** - Initial camera position ```swift .back // Rear camera (recommended) .front // Front camera ``` **captureSessionId** - Capture session ID ```swift let captureSessionId: UUID // Session ID created via the server API ``` **callback** - Callback function that receives the capture result ```swift typealias CameraResultCallback = (_ result: CameraResult) -> Void ``` `initializeCamera()` is an asynchronous function, so it must be called with `await`. In SwiftUI, using the `.task` modifier is recommended. When the function finishes, camera initialization is complete, and the `isInitialized` property of `CameraController` is also set to `true`. The previous approach of passing the license via `initializeCamera(licenseInfo:…)` is deprecated. As shown above, **inject `licenseInfo` into the initializer** and call `initializeCamera` without the license argument. *** ## Step 3: Handle Capture Results [#step-3-handle-capture-results] In Step 2, `CameraResult` is delivered through the `initializeCamera()` callback. Now you implement the inside of this callback. The callback may be invoked on a background thread. Perform UI work — updating `@State`/`@Published`, navigation, etc. — on the main thread (this applies to SwiftUI too). e.g. `await MainActor.run { ... }` or `DispatchQueue.main.async { ... }`. ```swift try await controller.initializeCamera( initialPosition: .back, captureSessionId: captureSessionId ) { result in // [!code highlight] switch result { // [!code highlight] case let .success(fingerprintImages, appearanceImages): // [!code highlight] // Success: use the image URL arrays print("Nose-print: \(fingerprintImages.count) images") print("Appearance: \(appearanceImages.count) images") // Upload to the server or move to the next screen uploadImages(fingerprintImages, appearanceImages) case .fail: // [!code highlight] // Failure: retry or dismiss the screen showRetryOrCancelAlert() // [!code highlight] } } ``` When you receive `CameraResult.fail`, the server's capture session is still open. * **Retry**: Call `controller.startDetection()` to restart capture within the same session * **Exit**: Close the camera screen and return to the previous screen If you close the screen without retrying, the server automatically terminates the session (ABORTED) after about 5 minutes. ### CameraResult Type [#cameraresult-type] ```swift public enum CameraResult { case success( fingerprintImages: [URL], // Biometric recognition images (file:// URL) appearanceImages: [URL] // Appearance images (file:// URL) ) case fail // Capture failed } ``` ### Using the Image URLs [#using-the-image-urls] Captured images are stored in the app's temporary directory as `file://` URLs. ```swift // Convert an image to UIImage if let image = UIImage(contentsOfFile: fingerprintImages[0].path) { imageView.image = image } // Convert to Data and upload to the server if let imageData = try? Data(contentsOf: fingerprintImages[0]) { await uploadToServer(imageData) } ``` **Image Storage Location** Captured images are stored in the app's temporary directory. If needed, copy them to another location, or delete them after uploading to the server. *** ## Step 4: Handle Errors [#step-4-handle-errors] `initializeCamera()` can throw various errors. Handle each error appropriately. ```swift private func initializeCamera() async { do { try await controller.initializeCamera( initialPosition: .back, captureSessionId: captureSessionId ) { result in /* ... */ } } catch PetnowUIError.invalidLicense(let underlyingError) { // License validation failed showError("Invalid API key.\n\(underlyingError.localizedDescription)") } catch PetnowUIError.multipleApiKeys { // A different API key was already used in this process showError("This app is already running with another API key.\nRestart the app to use a different one.") } catch PetnowUIError.permissionDenied(let message) { // Camera permission denied showError("Camera permission is required.\nPlease grant the permission in Settings.") showSettingsAlert() } catch { // Other errors showError("Camera initialization failed: \(error.localizedDescription)") } } private func showSettingsAlert() { if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) } } ``` ### PetnowUIError Type (Main Cases) [#petnowuierror-type-main-cases] ```swift public enum PetnowUIError: Error { case invalidLicense(underlyingError: Error) // API key error case notInitialized // Used before initialization case permissionDenied(message: String) // Permission denied case multipleApiKeys(first: String, second: String) // A different API key in the same process // There are additional network/response-related cases. } ``` A second, **different** API key in the same process is refused with `multipleApiKeys` instead of silently replacing the first one — the first key keeps working. The console can issue several keys, but one process must use one of them; restart the app to run against another. **Handling Permission Errors Is Required** When a `permissionDenied` error occurs, you must direct the user to the Settings app. Otherwise, the user will not be able to use the camera. *** ## Step 5: Additional Features [#step-5-additional-features] `CameraController` exposes the real-time capture state through `@Published` properties. You can use these to build a custom UI. ### Key @Published Properties [#key-published-properties] ```swift // Representative detection status (a detection is immediate; other states are smoothed) @Published public var detectionStatus: DetectionStatus // Progress (0–100, Int) @Published public var currentDetectionProgress: Int // Detected region (normalized coordinates 0.0–1.0) @Published public var detectedObjectNormalizedRect: CGRect? // Nose/face BoundingBox @Published public var detectionResult: DetectionResult? // Whether the camera switch button is enabled @Published public var isSwitchButtonEnabled: Bool // Whether initialization is complete @Published public var isInitialized: Bool // Live performance figures for diagnostics (camera init ms, first frame ms, fps, detector ms) @Published public private(set) var performanceStats: CameraPerformanceStats ``` `CameraController` does not expose the camera permission status or the current camera orientation as public properties. Check permissions directly in your app with `AVCaptureDevice` (see Best Practices below). ### DetectionStatus Type [#detectionstatus-type] ```swift public enum DetectionStatus { case noObject // No target detected case processing // Detection in progress case detected // Detection successful case failed(reason: DetectionFailureReason) // Failed (includes reason) case finished // Capture complete } public enum DetectionFailureReason { case error, tooBright, tooDark, noseNotFound case notFrontFace, notFrontCatFaceHor, notFrontCatFaceTop, notFrontCatFaceBottom case tooFarAway, tooClose, notFrontNoseTop, tooBlurred case shadowDetected, glareDetected, motionBlurDetected, defocusedBlurDetected case notFrontNose, furDetected, humanFaceDetected, fakeDetected, unexpected } ``` Example of mapping the status to a UI message: ```swift switch controller.detectionStatus { case .noObject: statusLabel.text = "Please fit your pet into the frame" case .processing: statusLabel.text = "Detecting..." case .detected: statusLabel.text = "Great! Hold still" case .finished: statusLabel.text = "Capture complete" case .failed(let reason): statusLabel.text = (reason == .tooClose) ? "Please move back a little" : "Please adjust the pose" } ``` ### Switching Between Front and Rear Cameras [#switching-between-front-and-rear-cameras] ```swift if controller.isSwitchButtonEnabled { controller.switchCamera() // [!code highlight] } ``` When `isSwitchButtonEnabled` is `false`, the device does not support a front camera, or the switch is not ready yet. ### Re-running / Pausing / Resuming Detection [#re-running--pausing--resuming-detection] ```swift controller.startDetection() // Start detection — also restarts from the beginning (re-capture) // [!code highlight] controller.pauseDetection() // Pause detection only controller.resumeDetection() // Resume from where it was paused ``` *** ## Using It in UIKit [#using-it-in-uikit] In a UIKit app, integrate `CameraView` using a `UIHostingController`. `CameraView(controller:)` is of type `CameraView`. ```swift class PetCameraViewController: UIViewController { private var controller: CameraController! private var hostingController: UIHostingController>? // [!code highlight] private let captureSessionId: UUID init(captureSessionId: UUID) { self.captureSessionId = captureSessionId super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() // [!code highlight:14] // Create the CameraController (settings + license) controller = CameraController( configuration: DetectionConfiguration(species: .dog, purpose: .petProfileRegistration), licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY") ) // Wrap CameraView in a UIHostingController let cameraView = CameraView(controller: controller) let hosting = UIHostingController(rootView: cameraView) hostingController = hosting addChild(hosting) view.addSubview(hosting.view) hosting.view.frame = view.bounds hosting.didMove(toParent: self) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) Task { do { try await controller.initializeCamera( initialPosition: .back, captureSessionId: captureSessionId ) { [weak self] result in DispatchQueue.main.async { self?.handleCameraResult(result) } } controller.startDetection() } catch { print("Initialization failed: \(error)") } } } private func handleCameraResult(_ result: CameraResult) { switch result { case let .success(fingerprints, appearances): print("Capture succeeded: \(fingerprints.count) images") case .fail: print("Capture failed") } } deinit { controller?.finalizeCamera() } } ``` **Notes for UIKit Usage** The callback may be invoked on a background thread, so always perform UI updates on the main thread. *** ## Best Practices [#best-practices] ### 1. Manage Your API Key Securely [#1-manage-your-api-key-securely] ```swift // Bad: hardcoded in code let apiKey = "sk_live_abc123..." // Good: use Info.plist or environment variables extension Bundle { var petnowAPIKey: String { guard let key = infoDictionary?["PETNOW_API_KEY"] as? String, !key.isEmpty else { fatalError("PETNOW_API_KEY not configured in Info.plist") } return key } } // Usage LicenseInfo(apiKey: Bundle.main.petnowAPIKey) ``` ### 2. Clean Up Resources [#2-clean-up-resources] ```swift // SwiftUI CameraView(controller: controller) .onDisappear { controller.finalizeCamera() } // UIKit deinit { controller?.finalizeCamera() } ``` ### 3. Check Permissions in Advance (Optional) [#3-check-permissions-in-advance-optional] ```swift import AVFoundation func checkCameraPermission() async -> Bool { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: return true case .notDetermined: return await AVCaptureDevice.requestAccess(for: .video) case .denied, .restricted: return false @unknown default: return false } } ``` *** ## Troubleshooting [#troubleshooting] ### Q. The camera won't initialize [#q-the-camera-wont-initialize] **A.** Check the following: 1. Verify that the API key is correct 2. Whether `NSCameraUsageDescription` has been added to `Info.plist` 3. Test on a real device (the simulator does not support the camera) ### Q. Capture never completes [#q-capture-never-completes] **A.** Try the following: 1. Capture in a bright location 2. Maintain an appropriate distance between the camera and the pet (30–50 cm) 3. Capture steadily so the pet does not move ### Q. After a capture failure, the screen stays frozen [#q-after-a-capture-failure-the-screen-stays-frozen] **A.** If you don't handle the UI after receiving `CameraResult.fail`, the camera screen stays frozen. Be sure to implement handling that either retries with `startDetection()` or returns to the previous screen by calling `dismiss()`. ### Q. How do I use the image URLs? [#q-how-do-i-use-the-image-urls] **A.** Captured images are stored in the temporary directory as `file://` URLs: ```swift case let .success(fingerprintImages, appearanceImages): if let firstImage = UIImage(contentsOfFile: fingerprintImages[0].path) { // Use the image } if let imageData = try? Data(contentsOf: fingerprintImages[0]) { // Use the Data (server upload, etc.) } ``` ## Next Steps [#next-steps] Once you've learned the basics, refer to the following documents: * [Customization](/petify/docs/sdk/ios/customization) - Overlay customization * [Sound Guide](/petify/docs/sdk/ios/sound) - Playing sounds # UI Customization Source: /petify/docs/sdk/ios/customization > How to customize the CameraView UI to match your app's design. **Prerequisites** This document assumes you have already read [Basic Usage](/petify/docs/sdk/ios/basic-usage). Learn how to create and initialize a `CameraController` first. *** ## Overview [#overview] `CameraView` provides the pet detection camera UI, and you can customize it in the following ways: 1. **Add overlay UI** - Place UI on top of CameraView with a ZStack (progress, buttons, guidance, etc.) 2. **Place a guide over the tracker** - UI that is automatically positioned over the detected object using `floatingGuideContent` 3. **Fully custom implementation** - Build from scratch by injecting your own `AVCaptureSession` In most cases, combining approaches 1 and 2 is sufficient. *** ## Overlay UI Development Guide [#overlay-ui-development-guide] You can place additional UI on top of `CameraView` using a ZStack. Most UI — buttons, progress indicators, status messages, and so on — is implemented this way. ### Basic Pattern [#basic-pattern] ```swift struct CameraScreenView: View { @ObservedObject var controller: CameraController @Environment(\.dismiss) var dismiss var body: some View { ZStack { CameraView(controller: controller) { EmptyView() // or floatingGuideContent } VStack { HStack { Button("Close") { dismiss() } Spacer() } Spacer() statusOverlay } .padding() } } @ViewBuilder private var statusOverlay: some View { VStack(spacing: 12) { Text(statusMessage) .font(.headline) .foregroundColor(.white) .padding() .background(Color.black.opacity(0.7)) .cornerRadius(12) if case .processing = controller.detectionStatus, controller.currentDetectionProgress > 0 { ProgressView(value: Double(controller.currentDetectionProgress) / 100.0) .progressViewStyle(LinearProgressViewStyle(tint: .white)) .frame(maxWidth: 300) Text("\(controller.currentDetectionProgress)%") .font(.caption) .foregroundColor(.white.opacity(0.8)) } } } private var statusMessage: String { switch controller.detectionStatus { case .noObject: return "Please fit your pet into the screen" case .processing: return "Detecting..." case .detected: return "Perfect! Please wait a moment" case .finished: return "Capture complete" case .failed(let reason): return failureMessage(for: reason) } } private func failureMessage(for reason: DetectionFailureReason) -> String { switch reason { case .tooFarAway: return "Please move a little closer" case .tooClose: return "Too close" case .tooBright: return "Too bright" case .tooDark: return "Lighting is too dark" case .tooBlurred: return "Blur detected" case .notFrontFace: return "Please face the camera" case .notFrontCatFaceHor: return "Turn the cat's face toward the camera" case .notFrontCatFaceTop: return "Lower the cat's head a little" case .notFrontCatFaceBottom: return "Raise the cat's head a little" default: return "Please try again" } } } ``` **Key points:** * Wrap CameraView in a ZStack to place UI freely * Observe the state of `CameraController` with `@ObservedObject` to update the UI dynamically * The `detectionStatus` switch must handle all 5 cases, including `.finished` ### Example: Color and Icon by Status [#example-color-and-icon-by-status] ```swift private var statusIcon: String { switch controller.detectionStatus { case .noObject: return "viewfinder" case .processing: return "camera.metering.center.weighted" case .detected: return "checkmark.circle.fill" case .finished: return "checkmark.seal.fill" case .failed: return "exclamationmark.triangle.fill" } } private var statusColor: Color { switch controller.detectionStatus { case .noObject: return .gray case .processing: return .blue case .detected: return .green case .finished: return .green case .failed: return .red } } ``` ### Example: Bounding Box Visualization [#example-bounding-box-visualization] If you want to visually highlight the detected region, you can use `detectedObjectNormalizedRect`. ```swift import SwiftUI import PetnowUI struct CameraWithBoundingBoxView: View { @ObservedObject var controller: CameraController var body: some View { ZStack { CameraView(controller: controller) { EmptyView() } GeometryReader { geometry in if let normalizedRect = controller.detectedObjectNormalizedRect { let box = convertToPixelRect(normalizedRect: normalizedRect, viewSize: geometry.size) Rectangle() .stroke(borderColor, lineWidth: 3) .frame(width: box.width, height: box.height) .position(x: box.midX, y: box.midY) .animation(.easeInOut(duration: 0.3), value: normalizedRect) } } } } private var borderColor: Color { switch controller.detectionStatus { case .detected: return .green case .processing: return .yellow case .failed: return .red default: return .gray // noObject, finished } } private func convertToPixelRect(normalizedRect: CGRect, viewSize: CGSize) -> CGRect { let videoAspectRatio: CGFloat = 3.0 / 4.0 let scaledHeight = viewSize.height let scaledWidth = scaledHeight * videoAspectRatio let xOffset = (viewSize.width - scaledWidth) / 2 let drawingRect = CGRect(x: xOffset, y: 0, width: scaledWidth, height: scaledHeight) return CGRect( x: drawingRect.origin.x + (normalizedRect.origin.x * drawingRect.width), y: drawingRect.origin.y + (normalizedRect.origin.y * drawingRect.height), width: normalizedRect.width * drawingRect.width, height: normalizedRect.height * drawingRect.height ) } } ``` If you need a more precise box for the detected nose/face, you can use `controller.detectionResult` (`DetectionResult` — `nose`/`face` `BoundingBox`). *** ## Placing UI Over the Tracker with floatingGuideContent [#placing-ui-over-the-tracker-with-floatingguidecontent] If you pass a `@ViewBuilder` closure to the `CameraView` initializer, the UI is automatically positioned **directly over** the detected object (the tracker). * **Automatic positioning**: Placed above the detected object (moves below if it overlaps) * **Screen boundary correction**: Automatically clamped so it does not go off-screen * **Center alignment**: Positioned relative to the center of the bounding box ### Example: Basic Text Guide [#example-basic-text-guide] ```swift CameraView(controller: controller) { Text("Please center the nose") .font(.headline) .foregroundColor(.white) .padding() .background(Color.black.opacity(0.7)) .cornerRadius(8) } ``` ### Example: Status-Driven Dynamic Guide [#example-status-driven-dynamic-guide] ```swift CameraView(controller: controller) { guideContent } @ViewBuilder private var guideContent: some View { HStack(spacing: 12) { Image(systemName: statusIcon) .font(.title2) .foregroundColor(.white) Text(statusMessage) .font(.headline) .foregroundColor(.white) } .padding() .background(statusColor.opacity(0.8)) .cornerRadius(12) .animation(.easeInOut(duration: 0.3), value: controller.detectionStatus) } private var statusMessage: String { switch controller.detectionStatus { case .noObject: return "Looking for your pet..." case .processing: return "Detecting..." case .detected: return "Done!" case .finished: return "Capture complete" case .failed: return "Try again" } } ``` ### Example: Species-Specific Guide [#example-species-specific-guide] Since the species (`Species`) is already known when the app creates the `DetectionConfiguration`, pass it to the screen as a parameter and use it. ```swift import SwiftUI import PetnowUI struct SpeciesGuideView: View { @ObservedObject var controller: CameraController let species: Species // Receives the species set by the app // [!code highlight] var body: some View { CameraView(controller: controller) { VStack(spacing: 12) { Image(systemName: species == .dog ? "pawprint.fill" : "cat.fill") .font(.system(size: 40)) .foregroundColor(.white) Text(species == .dog ? "Hold the dog's nose close" : "Center the cat's face from the front") .font(.headline) .foregroundColor(.white) .multilineTextAlignment(.center) } .padding() .background((species == .dog ? Color.blue : Color.orange).opacity(0.8)) .cornerRadius(16) } } } ``` `CameraController` does not expose `species` as a public property. Pass the species information directly from your app, as shown above. *** ## Pausing / Resuming Detection [#pausing--resuming-detection] While the camera is running, you can temporarily pause detection only and then resume it. This is useful while showing a tips screen or a guidance modal. ```swift // Pause detection before showing the tips screen controller.pauseDetection() isShowingTips = true // Resume detection when the tips sheet is dismissed .sheet(isPresented: $isShowingTips, onDismiss: { controller.resumeDetection() }) { TipsSheetView() } ``` **startDetection / pauseDetection / resumeDetection** * `startDetection()` — Resets progress to 0 and starts a new Detection Session (re-capture). * `pauseDetection()` — Temporarily pauses detection only. Keeps the camera preview and progress. * `resumeDetection()` — Resumes from where it was paused. For camera teardown, use `finalizeCamera()`. (The previous `stopDetection()` / `startDetectionSession()` are deprecated.) *** ## Fully Custom UI Implementation [#fully-custom-ui-implementation] This is how to build the UI from scratch by injecting your own `AVCaptureSession` without `CameraView`. Use this only when you need a **completely independent design**. **In most cases, overlay/floatingGuideContent is sufficient** This section is for special situations where `CameraView` cannot be used at all. If you have a SwiftUI app, consider the earlier approaches first. For React Native integration, use the separate [official RN package](/petify/docs/sdk/react-native/getting-started). ### Core Principle [#core-principle] `CameraController` can be **injected with your own `AVCaptureSession`** through its initializer. By using this session directly in a preview layer, you can render the video that the SDK fills in yourself. ```swift let session = AVCaptureSession() let controller = CameraController( configuration: DetectionConfiguration(species: .dog, purpose: .petProfileRegistration), licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY"), captureSession: session // Inject your own session // [!code highlight] ) // Build the preview layer with the same session, and subscribe to state via @Published properties. ``` `CameraController` **does not expose `captureSession` via a getter**. Keep the `session` instance your app created, as shown above, and reuse it for the preview. ### Starting and cleaning up the session [#starting-and-cleaning-up-the-session] **Inject a bare `AVCaptureSession()` and leave it alone.** Do not add inputs (`AVCaptureDeviceInput`)/outputs or call `session.startRunning()` yourself — the SDK configures and starts the session inside `initializeCamera()` (touching it yourself causes conflicts). Adding the preview layer alone shows nothing. The injected session is **started by `initializeCamera()`**, and detection begins with `startDetection()` — call both when the screen appears, and clean up with `finalizeCamera()` when it goes away. (This is the call most easily missed in the custom path, just like with `CameraView`.) ```swift // From the screen's onAppear / viewDidAppear, etc. Task { do { try await controller.initializeCamera( initialPosition: .back, captureSessionId: captureSessionId // UUID issued by the server ) { result in // Capture result (update UI on the main thread) } controller.startDetection() } catch { /* handle permission/license errors */ } } // On teardown (onDisappear / deinit) controller.finalizeCamera() ``` ### Minimal SwiftUI Implementation [#minimal-swiftui-implementation] ```swift import SwiftUI import AVFoundation import PetnowUI struct MinimalCustomCameraView: View { @ObservedObject var controller: CameraController let session: AVCaptureSession // The same instance injected into the controller // [!code highlight] var body: some View { ZStack { CameraPreviewLayer(session: session) .edgesIgnoringSafeArea(.all) VStack { Spacer() Text(statusText) .padding() .background(Color.black.opacity(0.7)) .foregroundColor(.white) .cornerRadius(8) } } } private var statusText: String { switch controller.detectionStatus { case .noObject: return "Please fit your pet into the screen" case .processing: return "Detecting... \(controller.currentDetectionProgress)%" case .detected: return "Done!" case .finished: return "Capture complete" case .failed(let reason): return "Failed: \(reason)" } } } // Display an AVCaptureSession in SwiftUI struct CameraPreviewLayer: UIViewRepresentable { let session: AVCaptureSession func makeUIView(context: Context) -> UIView { let view = UIView() let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) DispatchQueue.main.async { previewLayer.frame = view.bounds } return view } func updateUIView(_ uiView: UIView, context: Context) { if let layer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer { DispatchQueue.main.async { layer.frame = uiView.bounds } } } } ``` **Key points:** * Inject the `session` your app created into `CameraController(... captureSession:)`, and display the same instance with `AVCaptureVideoPreviewLayer` * Subscribe to `@Published` properties (`detectionStatus`, `currentDetectionProgress`, `detectedObjectNormalizedRect`, etc.) to react to state changes ### Minimal UIKit Implementation [#minimal-uikit-implementation] ```swift import UIKit import AVFoundation import PetnowUI import Combine class CustomCameraViewController: UIViewController { private let controller: CameraController private let session: AVCaptureSession private let captureSessionId: UUID // session ID issued by the server private var previewLayer: AVCaptureVideoPreviewLayer! private var cancellables = Set() private let statusLabel = UILabel() init(controller: CameraController, session: AVCaptureSession, captureSessionId: UUID) { self.controller = controller self.session = session self.captureSessionId = captureSessionId super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) statusLabel.textAlignment = .center statusLabel.textColor = .white view.addSubview(statusLabel) controller.$detectionStatus .sink { [weak self] status in self?.updateStatus(status) } .store(in: &cancellables) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() previewLayer.frame = view.bounds // keep the preview frame in sync on rotation/relayout statusLabel.frame = CGRect(x: 0, y: view.bounds.maxY - 80, width: view.bounds.width, height: 40) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // The injected session is started by initializeCamera(). Task { do { try await controller.initializeCamera( initialPosition: .back, captureSessionId: captureSessionId ) { [weak self] result in DispatchQueue.main.async { /* handle result (CameraResult) */ } } controller.startDetection() } catch { print("init failed: \(error)") } } } deinit { controller.finalizeCamera() } // clean up on teardown private func updateStatus(_ status: DetectionStatus) { switch status { case .noObject: statusLabel.text = "Please fit your pet into the screen" case .processing: statusLabel.text = "Detecting..." case .detected: statusLabel.text = "Done!" case .finished: statusLabel.text = "Capture complete" case .failed(let reason): statusLabel.text = "Failed: \(reason)" } } } ``` **Key points:** * Add the injected `session` directly to `view.layer` * Subscribe to `@Published` state with Combine's `sink` *** ## Next Steps [#next-steps] Once you've mastered customization, check out the following: ### Recommended Learning Order [#recommended-learning-order] 1. [Sound Guide](/petify/docs/sdk/ios/sound) - Changing the capture sound ### References [#references] * [Basic Usage](/petify/docs/sdk/ios/basic-usage) - Basic CameraView integration * [UI Module Overview](/petify/docs/sdk/ios/overview) - Detailed component descriptions # Getting Started Source: /petify/docs/sdk/ios/getting-started > Guide to installing and initially configuring the Petnow iOS SDK. *** ## Overview [#overview] This guide walks you through installing the Petnow iOS SDK into your project and performing the initial setup, step by step. ## Prerequisites [#prerequisites] Before you begin, make sure you have the following ready: * An Xcode project targeting iOS 16.4 or later * Xcode 16.0 or later * A Petnow API key — issue one from the [Petify Console](/petify/docs/petify-console) (or contact [support@petnow.io](mailto:support@petnow.io)) ## Step 1: Install the SDK [#step-1-install-the-sdk] The Petnow iOS SDK is distributed via AWS CodeArtifact. ### Install the AWS CLI [#install-the-aws-cli] Install the AWS CLI from the following link: [https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) ### Issue credentials from the Petify Console [#issue-credentials-from-the-petify-console] Issue AWS CodeArtifact credentials from My Page in the [Petify Console](/petify/docs/petify-console), and set the environment variables with the provided values. (See the [Petify Console](/petify/docs/petify-console) doc for sign-up, payment-method registration, and key issuance.) Petify Console – AWS CodeArtifact credentials screen ```bash export AWS_ACCESS_KEY_ID= export AWS_SECRET_ACCESS_KEY= export AWS_DEFAULT_REGION= export PETNOW_CODEARTIFACT_DOMAIN= export PETNOW_AWS_ACCOUNT_ID= export PETNOW_IOS_SDK_REPOSITORY= ``` Instead of setting environment variables every session, you can store your AWS credentials permanently: ```bash $ aws configure AWS Access Key ID [None]: AWS Secret Access Key [None]: Default region name [None]: Default output format [None]: json ``` This stores them in `~/.aws/credentials` and `~/.aws/config`. Once configured, you only need to export the SDK-specific variables: ```bash export PETNOW_CODEARTIFACT_DOMAIN= export PETNOW_AWS_ACCOUNT_ID= export PETNOW_IOS_SDK_REPOSITORY= ``` ### Swift Package Manager (AWS CodeArtifact) [#swift-package-manager-aws-codeartifact] The iOS SDK is installed via Swift Package Manager (SPM). **CocoaPods support is planned.** **Choose one** of the following based on your project type: #### Option A: Package.swift project [#option-a-packageswift-project] For a project that has a Package.swift file, you only need to run the following commands: 1. Move to the project root directory: ```bash cd /path/to/your-project # the directory containing Package.swift ``` 2. Log in to AWS CodeArtifact (**run this from a directory that contains `Package.swift`** — it invokes `swift package-registry set` internally): ```bash aws codeartifact login --tool swift \ --domain $PETNOW_CODEARTIFACT_DOMAIN \ --domain-owner $PETNOW_AWS_ACCOUNT_ID \ --repository $PETNOW_IOS_SDK_REPOSITORY \ --namespace petnow ``` `--namespace petnow` configures a **scoped registry** matching the SDK's package scope (`petnow.ui`, etc.); without it, every package would be routed to this registry. The scoped registry mapping is written to the **project's** `.swiftpm/configuration/registries.json`. 3. Confirm success: ```bash # Output on success: Successfully configured swift to use AWS CodeArtifact repository ... Login expires in 12 hours ... # Check the project registry file: cat .swiftpm/configuration/registries.json ``` The **token is valid for 12 hours**; re-run the login command when it expires. The token value is **not** stored in `registries.json` — it is kept in the **macOS keychain** (the file only records the registry URL and the auth method). In CI/automated environments, inject the AWS credentials as secrets and **run `aws codeartifact login` on every build** to refresh the token — see the [AWS CodeArtifact authentication tokens docs](https://docs.aws.amazon.com/codeartifact/latest/ug/tokens-authentication.html). #### Option B: Xcode Workspace [#option-b-xcode-workspace] If you use an Xcode Workspace, follow these steps: **Step 1: Generate the registry configuration** Create a dummy Package.swift in a temporary directory to generate the registry configuration: ```bash # Create a temporary directory mkdir -p ~/tmp/swift-registry-setup cd ~/tmp/swift-registry-setup # Create a dummy Package.swift cat > Package.swift << 'EOF' // swift-tools-version:5.7 import PackageDescription let package = Package( name: "DummyPackage", products: [], dependencies: [], targets: [] ) EOF # Log in to AWS CodeArtifact (this dummy directory = where Package.swift lives) aws codeartifact login --tool swift \ --domain $PETNOW_CODEARTIFACT_DOMAIN \ --domain-owner $PETNOW_AWS_ACCOUNT_ID \ --repository $PETNOW_IOS_SDK_REPOSITORY \ --namespace petnow ``` **Step 2: Copy the registry configuration** The scoped registry mapping is created in the **dummy project's** `.swiftpm/configuration/registries.json` (not `~/.swiftpm`). Copy that file into your Xcode Workspace: ```bash # (from the dummy directory) check the generated registry configuration cat .swiftpm/configuration/registries.json # Copy into the Xcode Workspace mkdir -p /path/to/YourApp.xcworkspace/xcshareddata/swiftpm/configuration cp .swiftpm/configuration/registries.json /path/to/YourApp.xcworkspace/xcshareddata/swiftpm/configuration/ ``` **Verify:** ```bash ls -la YourApp.xcworkspace/xcshareddata/swiftpm/configuration/registries.json ``` This `registries.json` does **not** contain the auth token — it only records the registry URL and the auth method (`type: token`). The actual token is stored by `aws codeartifact login` in the **macOS keychain** and is valid for **12 hours**. Re-run the login when it expires (or on every CI build). ### Installing in CI/CD (keychain-free) [#installing-in-cicd-keychain-free] CI runners can't rely on the macOS keychain. Instead of `aws codeartifact login` (which uses the keychain), **inject the token via `~/.netrc`** — this works headlessly. Put the AWS credentials in CI **secrets**, and because the token expires every 12 hours, **issue it on each build**. ```yaml # .github/workflows/ios.yml (example) jobs: build: runs-on: macos-15 env: AWS_ACCESS_KEY_ID: ${{ secrets.PETNOW_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.PETNOW_AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: ${{ secrets.PETNOW_AWS_REGION }} PETNOW_CODEARTIFACT_DOMAIN: ${{ secrets.PETNOW_CODEARTIFACT_DOMAIN }} PETNOW_AWS_ACCOUNT_ID: ${{ secrets.PETNOW_AWS_ACCOUNT_ID }} PETNOW_IOS_SDK_REPOSITORY: ${{ secrets.PETNOW_IOS_SDK_REPOSITORY }} steps: - uses: actions/checkout@v4 - name: Configure CodeArtifact SwiftPM registry (netrc) run: | ENDPOINT=$(aws codeartifact get-repository-endpoint \ --domain "$PETNOW_CODEARTIFACT_DOMAIN" --domain-owner "$PETNOW_AWS_ACCOUNT_ID" \ --repository "$PETNOW_IOS_SDK_REPOSITORY" --format swift \ --query repositoryEndpoint --output text) HOST=$(printf '%s' "$ENDPOINT" | sed -E 's#https?://([^/]+)/.*#\1#') TOKEN=$(aws codeartifact get-authorization-token \ --domain "$PETNOW_CODEARTIFACT_DOMAIN" --domain-owner "$PETNOW_AWS_ACCOUNT_ID" \ --query authorizationToken --output text) # token via netrc (no keychain). Valid for 12 hours. printf 'machine %s login aws password %s\n' "$HOST" "$TOKEN" > ~/.netrc chmod 600 ~/.netrc # register the scoped registry (run where Package.swift exists) swift package-registry set "$ENDPOINT" --scope petnow - name: Resolve & build run: swift package resolve --netrc && swift build --netrc ``` This flow (`get-authorization-token` → `~/.netrc` → `swift package-registry set --scope petnow` → `swift package resolve --netrc`) was verified against the live registry. An **Xcode app (`.xcworkspace`)** uses `xcodebuild` instead of `swift build`, with the registry config placed in the workspace (`xcshareddata/swiftpm/configuration/registries.json` — see Option B). Do not commit `~/.netrc`. ### Adding the package in Xcode [#adding-the-package-in-xcode] SPM setup screenshot 1. Open your project in Xcode 2. Select **File > Add Packages...** 3. Search for the `petnow.ui` package in the configured CodeArtifact repository 4. Set **Dependency Rule** to the version you want — `1.5.0` for this guide. The version in the screenshot is only an example. 5. Click **Add Package** > **Important**: After typing the package name, you must press Enter. If you don't, the search won't run. ### Verify the installation [#verify-the-installation] Once the package is added successfully, you can confirm it in the **Package Dependencies** section of the project navigator. ## Step 2: Configure the project [#step-2-configure-the-project] ### Add Info.plist permissions [#add-infoplist-permissions] For the SDK to use the camera, you must add a permission description to your `Info.plist`. Add the "Privacy - Camera Usage Description" or "NSCameraUsageDescription" key, and enter a string explaining why the camera is used. Info.plist editing #### Required permissions [#required-permissions] ```xml NSCameraUsageDescription Camera access is required to detect and identify pets. ``` > **Important**: Without a permission description, the app will crash when accessing the camera. ### Import the module [#import-the-module] ```swift import PetnowUI ``` ## Step 3: Create a capture session [#step-3-create-a-capture-session] **Required**: Before initializing the camera, you must create a capture session on your server and obtain a `captureSessionId`. ```swift // Obtain the captureSessionId from your server // (the server calls createCaptureSession in the Petnow Server API) let captureSessionId: UUID = await yourServerAPI.createCaptureSession( species: "DOG", purpose: "PET_PROFILE_REGISTRATION" ) ``` **Server API**: The `captureSessionId` is created on your server through the [Petnow Server API](/petify/docs/server/api/overview). It is not created directly on the client. Creating the capture session is **your app server's responsibility** (the client only uses the issued `captureSessionId`). The `petId` requirement depends on the purpose (required for registration/verification, not required for identification). For the creation parameters, the `petId` requirement, and the full server flow through result-image upload, see [Server API – Biometric](/petify/docs/server/api/biometric). For more on session concepts, see the [UI Module Overview](/petify/docs/sdk/ios/overview#session-concepts). ## Step 4: Initialize the camera [#step-4-initialize-the-camera] Initialize the camera with PetnowUI's `CameraController`. Pass the capture settings (`DetectionConfiguration`) and the API key (`LicenseInfo`) to the **controller's initializer**, then call `initializeCamera` afterward. ```swift import SwiftUI import PetnowUI struct PetCameraView: View { @StateObject private var controller: CameraController private let captureSessionId: UUID // the session ID received from the server init(captureSessionId: UUID) { self.captureSessionId = captureSessionId _controller = StateObject(wrappedValue: CameraController( configuration: DetectionConfiguration( species: .dog, // .dog or .cat purpose: .petProfileRegistration // .petProfileRegistration / .petVerification / .petIdentification ), licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY") )) } var body: some View { CameraView(controller: controller) .task { await initializeCamera() } } private func initializeCamera() async { do { try await controller.initializeCamera( initialPosition: .back, captureSessionId: captureSessionId ) { result in switch result { case let .success(fingerprintImages, appearanceImages): // Arrays of local file:// URLs — upload to your app server print("Capture complete: fingerprint \(fingerprintImages.count), appearance \(appearanceImages.count)") case .fail: print("Capture failed") } } controller.startDetection() // [!code highlight] } catch { print("Camera initialization failed: \(error)") } } } ``` `initializeCamera()` connects the camera but does **not** start detection — call `controller.startDetection()` once it returns, as above. ### DetectionConfiguration / LicenseInfo [#detectionconfiguration--licenseinfo] * `DetectionConfiguration(species:purpose:enableFakeDetection:)` — capture settings (`difficultyMode:` is still accepted but deprecated and ignored since 1.5.0). `purpose` is a `DetectionPurpose` (`.petProfileRegistration` / `.petVerification` / `.petIdentification`). * `LicenseInfo(apiKey:environment:)` — the API key and, optionally, the backend it belongs to. License validation is performed on the server at `initializeCamera` time (once per key). > The previous approach of passing the license via `initializeCamera(licenseInfo:…)` is deprecated. As shown above, **inject `licenseInfo` into the initializer** and call `initializeCamera` without the license argument. Spoof detection (photos, screens, printed images) is on by default. Passing `false` turns spoof detection off for that session, which is how you evaluate the SDK against photographs of pets instead of live animals. ### Backend environment (`environment`) [#backend-environment-environment] ```swift // public enum PetnowEnvironment { case production, staging } LicenseInfo(apiKey: "YOUR_STAGING_API_KEY", environment: .staging) ``` Omit `environment` to keep the environment the SDK was built for, which is **production** for every published build. Nothing changes for an app that does not set it. A key issued by the staging console is only valid against the staging backend and vice versa — a mismatch fails license validation. `environment` is a named environment on purpose: do **not** wire it to your own `DEBUG` flag. A release build that accidentally selected staging would send real captures to a backend where the pets are not registered. (This restores the capability v1.3.x had as `isDebugMode`, which was removed in v1.4.) The console can issue several API keys, but one process must use one of them. Initializing with a second, **different** API key in the same process throws `PetnowUIError.multipleApiKeys` and the first key keeps working. To run against another key, restart the app. For security, do not hardcode your API key directly in your code. **Method 1: Use an environment variable** ```swift import Foundation extension Bundle { var petnowAPIKey: String { guard let key = infoDictionary?["PETNOW_API_KEY"] as? String else { fatalError("PETNOW_API_KEY not found in Info.plist") } return key } } // Usage let licenseInfo = LicenseInfo( apiKey: Bundle.main.petnowAPIKey ) ``` **Method 2: Use an .xcconfig file** ```bash # Config/Debug.xcconfig PETNOW_API_KEY = your_debug_api_key # Config/Release.xcconfig PETNOW_API_KEY = your_production_api_key ``` Add to Info.plist: ```xml PETNOW_API_KEY $(PETNOW_API_KEY) ``` ## Step 5: Verify the installation [#step-5-verify-the-installation] To confirm the SDK is installed correctly, build and run your app. If camera initialization succeeds, the camera preview is shown. On failure, check the error message: ```swift do { // licenseInfo was already passed to the CameraController(configuration:licenseInfo:) initializer above try await controller.initializeCamera( initialPosition: .back, captureSessionId: captureSessionId ) { _ in print("Capture complete") } print("Camera initialization succeeded") } catch { print("Initialization failed: \(error.localizedDescription)") } ``` ## Troubleshooting [#troubleshooting] ### Package not found (Package Resolution Failed) [#package-not-found-package-resolution-failed] **Symptom**: Xcode shows "package 'PetnowUI' not found" or a similar error **Solution:** 1. **Check the registry configuration** ```bash # Xcode Workspace project cat YourApp.xcworkspace/xcshareddata/swiftpm/configuration/registries.json # Package.swift project (the .swiftpm in the project root) cat .swiftpm/configuration/registries.json ``` If the file is missing or empty, log in again **from a directory that contains `Package.swift`** (the Package.swift project root, or the Option B dummy project): ```bash cd /path/to/package-dir # the directory containing Package.swift aws codeartifact login --tool swift \ --domain $PETNOW_CODEARTIFACT_DOMAIN \ --domain-owner $PETNOW_AWS_ACCOUNT_ID \ --repository $PETNOW_IOS_SDK_REPOSITORY \ --namespace petnow ``` 2. **Check for token expiration** * Tokens expire after 12 hours * Run the `aws codeartifact login` command again 3. **Clear the Xcode cache** ```bash # Clear the SPM cache rm -rf ~/Library/Caches/org.swift.swiftpm rm -rf ~/Library/Developer/Xcode/DerivedData # After restarting Xcode, File > Packages > Reset Package Caches ``` ### Missing registry configuration (Xcode Workspace) [#missing-registry-configuration-xcode-workspace] **Symptom**: Xcode can't find the package even after logging in **Cause**: The scoped registry mapping is created in the **`.swiftpm/configuration/registries.json` of the project (or dummy project)** where you ran the login. The Xcode Workspace uses its own `xcshareddata/swiftpm/configuration/registries.json`, so you must copy it there for the workspace to recognize it. **Solution**: Perform the registry configuration copy from Option B (run from the dummy project directory) ```bash # Copy into the Xcode Workspace mkdir -p YourApp.xcworkspace/xcshareddata/swiftpm/configuration cp .swiftpm/configuration/registries.json YourApp.xcworkspace/xcshareddata/swiftpm/configuration/ ``` ### Camera permission error [#camera-permission-error] **Symptom**: The app crashes or no permission prompt is shown\ **Solution**: Verify that `NSCameraUsageDescription` is added to Info.plist ### API key error [#api-key-error] **Symptom**: Authentication error during camera initialization\ **Solution**: Verify that the API key is correct and contact the Petnow team ## API Key Security [#api-key-security] The key you pass to `LicenseInfo(apiKey:)` is the **same Petnow API key type** used for `x-petnow-api-key` on the [Server API](/petify/docs/server/api/overview). Treat it as a credential: anything shipped inside an app binary can be read back out of it. **Recommended setup** * Issue a **dedicated key for the mobile app**, separate from the key your server uses. An account can hold multiple active keys. * Do not embed the server key in the app. Where possible, fetch the app key from your own server at runtime instead of hardcoding it. **Revocation** * Keys are deactivated in the [Petify Console](/petify/docs/petify-console) (My Page → API Key Management). * Deactivation takes effect immediately and does not affect other keys. * A deactivated key cannot be reactivated — create a new one instead. **Zero-downtime rotation on an app release** 1. Create a new app key. 2. Ship the new app version with the new key. 3. Let older app versions drain. 4. Deactivate the old key. There is no automatic key rotation inside the SDK. The CodeArtifact / AWS access keys from Step 1 are used only to download the SDK at build time. They are unrelated to runtime authentication. ## Next steps [#next-steps] Installation and setup are complete! You're now ready to use the SDK. * [UI Module Overview](/petify/docs/sdk/ios/overview) - Understand the UI module structure * [Basic Usage](/petify/docs/sdk/ios/basic-usage) - Integrate the camera UI ## Support [#support] If you run into problems during installation, contact [support@petnow.io](mailto:support@petnow.io). # iOS SDK Introduction Source: /petify/docs/sdk/ios/intro > An introduction to the technical requirements and features of the Petnow iOS SDK. ## Latest Version [#latest-version] | Module | Version | | --------- | ------- | | UI Module | 1.5.0 | **Server API recommended**: Perform pet management tasks such as registration, verification, and identification on your server through the [Server API](/petify/docs/server/api/overview). The client is responsible only for capture via the UI module. ## iOS SDK Features [#ios-sdk-features] The Petnow iOS SDK is a native framework written in Swift that can be used with both SwiftUI and UIKit. ## SDK Structure [#sdk-structure] ### Key Components [#key-components] **UI Module** * `CameraView` * A SwiftUI View that displays the camera preview and detection overlay. * Takes an injected CameraController and displays detection results. * `CameraController` * Manages the camera session and detection logic, and publishes state. ### Usage Methods [#usage-methods] * **Swift Package Manager (SPM)** * Use via AWS CodeArtifact * **CocoaPods** — *support planned* ## Technical Requirements [#technical-requirements] * **Minimum iOS version**: iOS 16.4 or later * **Xcode**: 16.0 or later * **Swift**: 5.7 or later ## Next Steps [#next-steps] To integrate the SDK into your iOS app, refer to the following documents: * [Getting Started](/petify/docs/sdk/ios/getting-started) - Installation and initial setup * [UI Module](/petify/docs/sdk/ios/overview) - SwiftUI camera integration # UI Module Overview Source: /petify/docs/sdk/ios/overview > A description of the structure and core components of the PetnowUI module. *** ## What is the PetnowUI module? [#what-is-the-petnowui-module] PetnowUI is a module that provides a **ready-to-use camera UI** for capturing pet biometric data. It handles all the complex camera setup, detection logic, image processing, and more, so developers can integrate professional biometric recognition into their app with just a few lines of code. UI Module Intro ### Core Values [#core-values] * 🚀 **Fast integration**: No need to know the complex camera API * 🎯 **Real-time guidance**: Guides users to the optimal way to capture * 🎨 **Customizable**: Adjustable to match your app's design * 📱 **SwiftUI & UIKit support**: Usable in any iOS app ## Architecture [#architecture] The PetnowUI module hides the complex state management and ML framework integration logic, exposing only a simple interface to developers. ## Core Components [#core-components] ### CameraView [#cameraview] A SwiftUI View that displays the camera preview and detection overlay. **Responsibilities:** * Render the camera preview * Display the detection overlay * Handle user interaction **Simple usage example:** ```swift // Basic usage CameraView(controller: controller) // Add a custom overlay CameraView(controller: controller) { Text("Please center the nose") .foregroundColor(.white) .padding() .background(Color.black.opacity(0.7)) .cornerRadius(8) } ``` ### CameraController [#cameracontroller] A view model that is an `ObservableObject` and is injected into `CameraView`. #### Session concepts [#session-concepts] The capture process is managed with two kinds of sessions: **Capture Session** * Created by calling the `createCaptureSession` API on the server * Pass the created `captureSessionId` to `initializeCamera()` to start capturing * A single Capture Session can contain multiple Detection Sessions * Used in the Petify Console to track the entire capture process and analyze metrics **Detection Session** * The session from the moment actual detection starts until detection completes or fails * When the user re-captures, a new Detection Session starts > **Note**: A new Detection Session starts on re-capture. You can track each session individually in the Petify Console. ## State Management [#state-management] ### DetectionStatus [#detectionstatus] Represents the detection result state of the camera frame. A successful detection is reported immediately; the other states are reported as the representative value over a short window, so they do not flicker frame to frame. ```swift public enum DetectionStatus { case noObject // No target detected case processing // Detection in progress case detected // Detection succeeded case failed(reason: DetectionFailureReason) // Failed (includes reason) case finished // Capture complete } ``` ### CameraResult [#cameraresult] The final capture result. ```swift public enum CameraResult { case success( fingerprintImages: [URL], // Fingerprint/nose-print images appearanceImages: [URL] // Appearance images ) case fail // Capture failed } ``` ## Data Flow [#data-flow] > **Key point**: When `CameraController` publishes state, your custom camera screen uses those state values together with `CameraView` to build the UI. When capture completes, `CameraResult` is delivered via the callback. ### Data flow explanation [#data-flow-explanation] **1. Real-time state updates** * `CameraController` publishes the following state with `@Published`: * `detectionStatus`: `.noObject`, `.processing`, `.detected`, `.failed(reason:)`, `.finished` * `currentDetectionProgress`: 0 \~ 100 (`Int`, detection progress) * `detectedObjectNormalizedRect`: detected region (normalized coords), `detectionResult`: nose/face `BoundingBox` * `isSwitchButtonEnabled`, `isInitialized` * The custom camera screen subscribes to these state values to update the UI. **2. Final result delivery** * When detection completes, `CameraController` saves the images and creates a `CameraResult`. * `CameraResult.success` or `CameraResult.fail` is delivered through the callback. ## Best Practices [#best-practices] ### 1. ViewModel lifecycle management [#1-viewmodel-lifecycle-management] ```swift // SwiftUI: use @StateObject @StateObject private var viewModel = CameraController(...) // Cleanup .onDisappear { viewModel.finalizeCamera() } ``` ### 2. Error handling [#2-error-handling] ```swift do { try await viewModel.initializeCamera(...) } catch PetnowUIError.invalidLicense(let error) { // Handle license error } catch PetnowUIError.permissionDenied(let message) { // Handle permission error } catch { // Handle other errors } ``` ### 3. State observation [#3-state-observation] ```swift // Actions based on a specific state .onChange(of: viewModel.detectionStatus) { status in switch status { case .detected: playSuccessSound() case .failed(let reason): showErrorMessage(reason) default: break } } ``` ## Next Steps [#next-steps] Once you understand the structure of the UI module, refer to the following documents: * [Basic Usage](/petify/docs/sdk/ios/basic-usage) - Integrate CameraView * [Customization](/petify/docs/sdk/ios/customization) - Customize the overlay * [Sound Guide](/petify/docs/sdk/ios/sound) - Detailed guide on sound playback ## References [#references] * [Getting Started](/petify/docs/sdk/ios/getting-started) - SDK installation * [Troubleshooting](/petify/docs/sdk/troubleshooting) - Problem resolution # Sound Guide Source: /petify/docs/sdk/ios/sound > Playing pet attention-grabbing sounds using SoundPlayer. *** ## Overview [#overview] The UI module bundles 6 sounds to grab a pet's attention. Using the `SoundPlayer` API, you can encourage a pet to look at the camera during capture. ## Available Sounds [#available-sounds] ### Full Sound List [#full-sound-list] **Sound Preview** Click each sound to preview it. Test your pet's reaction! | Sound | Description | Target | Preview | | ---------------- | ---------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- | | 🥔 `bagOfChips` | Rustling of a chip bag | 🐕🐱 Both | | | 🥫 `openingCan` | Sound of opening a can | 🐕🐱 Both | | | 🍚 `pouringFood` | Sound of pouring food | 🐕🐱 Both | | | 🥣 `shakeFood` | Sound of shaking food | 🐕🐱 Both | | | 🧸 `squeakyToy` | Squeaky toy sound | 🐕 Dog | | | 😽 `pspspsSound` | Pspsps sound | 🐱 Cat | | ## Basic Usage [#basic-usage] ### 1. Play a Random Sound [#1-play-a-random-sound] Automatically selects and plays a sound appropriate for the species. ```swift import PetnowUI // Random sound for dogs let player = try? SoundPlayer.play(.random(species: .dog)) // Random sound for cats let player = try? SoundPlayer.play(.random(species: .cat)) ``` ### 2. Play a Specific Sound [#2-play-a-specific-sound] Plays a specific sound of your choice. ```swift // Play the squeaky toy sound let player = try? SoundPlayer.play(.one(name: .squeakyToy)) // Play the pspsps sound let player = try? SoundPlayer.play(.one(name: .pspspsSound)) ``` ### 3. Stop a Sound [#3-stop-a-sound] To stop a sound, you must keep a reference to the `PlayerType` object. ```swift // ⚠️ Doing this means you can't call stop() try? SoundPlayer.play(.one(name: .squeakyToy)) // Return value ignored // ✅ Keep the player to be able to call stop() let player = try? SoundPlayer.play(.one(name: .squeakyToy)) // ... later player?.stop() ``` ### 4. Controlling When Playback Starts (SoundPlayer.make) [#4-controlling-when-playback-starts-soundplayermake] `SoundPlayer.play(_:)` starts playing immediately when called. To control when playback starts yourself, or to prepare a player object in advance, use `SoundPlayer.make(_:)`. ```swift // Create the player only (not yet playing) let player = try? SoundPlayer.make(.one(name: .bagOfChips)) // Play at the desired time player?.play() // Stop player?.stop() ``` **When should you use SoundPlayer.make?** * When you want to **prepare** a sound in advance and play it on a specific event * When you need to **stop** a sound that is playing * When you need to **repeatedly play** the same sound ### PlayerType Protocol [#playertype-protocol] Both `SoundPlayer.play(_:)` and `SoundPlayer.make(_:)` return a `SoundPlayer` that conforms to the `PlayerType` protocol. ```swift public protocol PlayerType { func play() // Play (from the beginning) func stop() // Stop and rewind to the beginning } ``` ## Next Steps [#next-steps] * [Basic Usage](/petify/docs/sdk/ios/basic-usage) - The full integration flow * [Customization](/petify/docs/sdk/ios/customization) - Overlay customization # API v1 to v2 Migration Source: /petify/docs/sdk/migration/api-v1-to-v2 > Guide for migrating from Petnow API v1 to v2. ## Overview [#overview] API v2 introduces the **Capture Session** concept, streamlining how fingerprint and appearance images are processed. This guide helps you migrate from v1 to v2. ## Key Changes [#key-changes] ### Capture Session Introduction [#capture-session-introduction] In v1, metadata (species, purpose) was passed with each file upload. In v2, you first create a capture session and then upload files within that session. | Aspect | API v1 | API v2 | | -------------- | ----------------------- | ---------------------------- | | Metadata | Passed with each upload | Set once at session creation | | File Grouping | Manual ID management | Automatic via session | | Action Request | List of file IDs | Session ID only | ### Endpoint Changes [#endpoint-changes] | Function | v1 Endpoint | v2 Endpoint | | ------------------ | --------------------------------------- | --------------------------------------- | | Fingerprint Upload | `POST /v1/fingerprints:upload` | `POST /v2/fingerprints:upload` | | Appearance Upload | `POST /v1/appearances:upload` | `POST /v2/appearances:upload` | | Pet Verify | `POST /v1/pets/{petId}:verify` | `POST /v2/pets/{petId}:verify` | | Pet Identify | `POST /v1/pets:identify` | `POST /v2/pets:identify` | | Add Fingerprints | `POST /v1/pets/{petId}:addFingerprints` | `POST /v2/pets/{petId}:addFingerprints` | | **New** | - | `POST /v2/capture-sessions` | ## Workflow Comparison [#workflow-comparison] ### v1 Workflow [#v1-workflow] ### v2 Workflow [#v2-workflow] **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, and all files in the session are automatically used when performing actions (verify, identify, addFingerprints). ## Code Migration Example [#code-migration-example] ### v1 Code (Before) [#v1-code-before] ```javascript // v1: Pass metadata with each upload const fp1 = await fetch('/v1/fingerprints:upload', { method: 'POST', body: createFormData({ file: file1, species: 'DOG', purpose: 'PET_VERIFICATION' }) }).then(r => r.json()); const fp2 = await fetch('/v1/fingerprints:upload', { method: 'POST', body: createFormData({ file: file2, species: 'DOG', purpose: 'PET_VERIFICATION' }) }).then(r => r.json()); // v1: Verify with file ID list const verifyResult = await fetch(`/v1/pets/${petId}:verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fingerprints: [fp1.data.id, fp2.data.id] }) }).then(r => r.json()); ``` ### v2 Code (After) [#v2-code-after] ```javascript // v2: Create session (metadata once) const session = await fetch('/v2/capture-sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ species: 'DOG', purpose: 'PET_VERIFICATION', petId: petId }) }).then(r => r.json()); // v2: Upload files (automatically linked with sessionId query parameter) const fp1 = await fetch(`/v2/fingerprints:upload?sessionId=${session.data.sessionId}`, { method: 'POST', body: createFormData({ file: file1 }) }).then(r => r.json()); const fp2 = await fetch(`/v2/fingerprints:upload?sessionId=${session.data.sessionId}`, { method: 'POST', body: createFormData({ file: file2 }) }).then(r => r.json()); // v2: Verify with session ID (all uploaded files automatically used) const verifyResult = await fetch(`/v2/pets/${petId}:verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: session.data.sessionId }) }).then(r => r.json()); ``` ## Session States [#session-states] Capture sessions have the following states: | State | Description | | ---------- | ---------------------------------------------- | | `STARTED` | Session is active and ready to receive uploads | | `FINISHED` | Session successfully closed with captured data | | `ABORTED` | Session was cancelled or failed | | `TIMEOUT` | Session expired without proper closure | ## Upload Requirements [#upload-requirements] | Operation | Minimum Fingerprints | | -------------- | -------------------- | | Registration | 5 | | Verification | 3 | | Identification | 3 | ## Migration Checklist [#migration-checklist] * [ ] Add session creation logic (`POST /v2/capture-sessions`) * [ ] Add `sessionId` query parameter to file upload URLs * [ ] Remove `species`, `purpose` metadata from file upload request body * [ ] Use session ID instead of file ID list in verify/identify requests * [ ] Remove session end logic (automatically handled) * [ ] Add session-related error handling * [ ] Update endpoint URLs from `/v1/` to `/v2/` ## v2 Benefits [#v2-benefits] 1. **Simplified Metadata**: Set species and purpose once at session creation 2. **Better Tracking**: All uploads for an operation are automatically grouped 3. **Enhanced Validation**: Minimum requirements checked before processing 4. **Extensibility**: Session-based architecture supports progress tracking, resumable uploads, etc. ## Common Migration Errors [#common-migration-errors] ### Upload Without Session [#upload-without-session] ```json { "error": "Session context is required" } ``` **Solution**: Create a capture session before uploading files. ### Missing sessionId Query Parameter [#missing-sessionid-query-parameter] ```json { "error": "Session ID is required" } ``` **Solution**: Add `?sessionId={sessionId}` query parameter to upload URL. ### Missing Pet ID for Verification [#missing-pet-id-for-verification] ```json { "error": "Pet ID is required for verification purpose" } ``` **Solution**: Include `petId` when creating the capture session. ## Related Documentation [#related-documentation] * [Server API Overview](/petify/docs/server/api/overview) * [Registration/Verification/Identification](/petify/docs/server/api/identification) # SDK v1.2 → v1.4 Migration Source: /petify/docs/sdk/migration/sdk-v1.2-to-v1.4 > Upgrade a v1.2.x integration directly to v1.4 — no need to pass through v1.3. Upgrade your dependency straight to v1.4 — there is no reason to install v1.3 on the way. This guide lists every code change between v1.2.x and v1.4 in one place, expressed against the **v1.4 API only**, so you never write intermediate v1.3-era code that v1.4 then replaces. Coming from v1.3.x instead? Use the shorter [SDK v1.3 → v1.4 Migration](/petify/docs/sdk/migration/sdk-v1.3-to-v1.4). Still calling the v1 Server API? Also follow [API v1 → v2](/petify/docs/sdk/migration/api-v1-to-v2). *** ## iOS [#ios] ### 1. `CameraViewModel` → `CameraController`, license in the constructor, `captureSessionId` required [#1-cameraviewmodel--cameracontroller-license-in-the-constructor-capturesessionid-required] Three v1.2-era patterns change in one stroke: the type is renamed, the license moves to the constructor, and `initializeCamera` now requires a `captureSessionId` issued by the server (`POST /v2/capture-sessions`). ```swift // ❌ v1.2.x @StateObject private var viewModel: CameraViewModel try await viewModel.initializeCamera( licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY"), initialPosition: .back ) { result in /* ... */ } CameraView(viewModel: viewModel) // ✅ v1.4 @StateObject private var controller = CameraController( // [!code highlight] configuration: DetectionConfiguration(species: .dog, purpose: .petProfileRegistration), // [!code highlight] licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY") // [!code highlight] ) let sessionId = try await createCaptureSessionFromServer() // POST /v2/capture-sessions try await controller.initializeCamera(initialPosition: .back, captureSessionId: sessionId) // [!code highlight] CameraView(controller: controller) // [!code highlight] ``` **Breaking**: without `captureSessionId` the call does not compile. Generate a session ID server-side before initializing the camera. → [Server API](/petify/docs/server/api/overview) **Why:** the controller owns the license for its lifetime (validation runs once), `initializeCamera` only connects the camera, and the type name matches Android's `CameraController`. → [Getting Started](/petify/docs/sdk/ios/getting-started) ### 2. `previewLayer` removed — `CameraView` renders the preview [#2-previewlayer-removed--cameraview-renders-the-preview] `CameraViewModel.previewLayer` (v1.2) is gone, and in v1.4 the capture session has no public getter — but you can still inject your own `AVCaptureSession` through the initializer for a fully custom preview. → [Customization](/petify/docs/sdk/ios/customization) `CameraView` owns preview rendering; to check readiness, use the published `isInitialized`. ```swift // ❌ v1.2.x if viewModel.previewLayer != nil { showCameraView() } view.layer.addSublayer(viewModel.previewLayer) // ✅ v1.4 — CameraView renders the preview; check readiness explicitly if controller.isInitialized { showCameraView() } // [!code highlight] CameraView(controller: controller) // [!code highlight] ``` To layer custom UI over the preview, place your views on top of `CameraView` — see [Customization](/petify/docs/sdk/ios/customization). ### 3. Detection no longer starts by itself — call `startDetection()` [#3-detection-no-longer-starts-by-itself--call-startdetection] `initializeCamera()` connects the camera but does **not** begin detection. After it returns, start (and re-start for a retake) explicitly: ```swift try await controller.initializeCamera(initialPosition: .back, captureSessionId: sessionId) controller.startDetection() // [!code highlight] ``` ### 4. Teardown: `stopDetection()` → `finalizeCamera()` [#4-teardown-stopdetection--finalizecamera] ```swift // ❌ v1.2.x controller.stopDetection() // ✅ v1.4 controller.finalizeCamera() // [!code highlight] ``` Pause/resume have their own verbs (`pauseDetection()`), so a full teardown is always explicit. ### iOS change summary [#ios-change-summary] | v1.2.x | v1.4 | | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | `CameraViewModel` / `CameraView(viewModel:)` | `CameraController` / `CameraView(controller:)` | | `initializeCamera(licenseInfo:initialPosition:)` | constructor `CameraController(configuration:licenseInfo:)` + `initializeCamera(initialPosition:captureSessionId:)` | | `previewLayer` | removed — `CameraView` renders the preview; readiness via `isInitialized` | | `stopDetection()` | `finalizeCamera()`; detection starts via an explicit `startDetection()` | *** ## Android [#android] ### 1. Global `PetnowApiClient` gone — license and configuration are passed per session [#1-global-petnowapiclient-gone--license-and-configuration-are-passed-per-session] ```kotlin // ❌ v1.2.x (the apiClient module is no longer shipped — compile error) PetnowApiClient.init(key = "YOUR_API_KEY", isDebugMode = false) PetnowApiClient.configureDetectionMode( purpose = DetectionPurpose.PET_PROFILE_REGISTRATION, species = PetSpecies.DOG, enableFakeDetection = true ) ``` * **Recommended (`CameraView` + `CameraController`)**: pass `LicenseInfo` to the `CameraController(context, license, scope)` constructor and `DetectionConfiguration` to `initializeCamera(config, captureSessionId)`. → [Basic Usage](/petify/docs/sdk/android/basic-usage) * **Keep `PetnowCameraFragment`**: pass the license via Fragment args (`ARG_API_KEY`) or `provideLicense()`, and the configuration via `ARG_DETECTION_CONFIGURATION`. → [Fragment (legacy)](/petify/docs/sdk/android/fragment) `PetnowApiClient.isSuccessInitialize` has no replacement — initialization failures surface as `PetnowUIError` (item 4). The configuration types move packages (`io.petnow.api.client.*` → `io.petnow.ui.config.*`, same shape), and `isDebugMode` is gone on Android. **If you called the Petnow Server API through `PetnowApiClient`** (capture sessions, uploads, registration): those helpers were retired with the module. Call the [Server API](/petify/docs/server/api/overview) from **your app server** — the v1.4 client SDK handles capture only. ### 2. `captureSessionId` is required [#2-capturesessionid-is-required] Whichever path you take, a server-issued `captureSessionId` (`POST /v2/capture-sessions`) is now mandatory. ```kotlin // ✅ v1.4 — CameraController path val sessionId = createCaptureSessionFromServer() controller.initializeCamera(configuration, captureSessionId = sessionId) // [!code highlight] // ✅ v1.4 — Fragment path val fragment = MyCameraFragment().apply { arguments = Bundle().apply { putString(ARG_CAPTURE_SESSION_ID, sessionId.toString()) // [!code highlight] } } ``` **Breaking**: without `captureSessionId` the Fragment terminates immediately and the controller path fails to initialize. ### 3. Listener import package changed — and move to V2 [#3-listener-import-package-changed--and-move-to-v2] ```kotlin // ❌ v1.2.x import io.petnow.ui.PetnowCameraDetectionListener // ✅ v1.4 import io.petnow.callback.PetnowCameraDetectionListenerV2 // [!code highlight] ``` On the `CameraController` path use `setDetectionListenerV2`: V2 delivers the same result models as iOS — a sealed `DetectionStatus` (`NoObject`/`Processing`/`Detected`/`Finished`/`Failed(reason)`) and `CameraResult` (`Success(fingerprintImageFiles, appearanceImageFiles)`/`Fail`). ### 4. Initialization failures are the structured `PetnowUIError` [#4-initialization-failures-are-the-structured-petnowuierror] `initializeCamera()` no longer leaks raw platform exceptions, and it validates the API key against the server (once per key per process). ```kotlin // ✅ v1.4 — a single catch + sealed when try { controller.initializeCamera(configuration, captureSessionId) } catch (e: PetnowUIError) { // [!code highlight] when (e) { is PetnowUIError.InvalidLicense -> { /* API key rejected */ } is PetnowUIError.PermissionDenied -> { /* permission missing */ } is PetnowUIError.CameraOpenFailed -> { /* camera error — see e.cause */ } } } ``` `CancellationException` still propagates untouched (normal lifecycle — rethrow it). ### Android change summary [#android-change-summary] | v1.2.x | v1.4 | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `PetnowApiClient.init()` / `configureDetectionMode()` | per-session: `CameraController(context, license, scope)` + `initializeCamera(config, captureSessionId)`, or Fragment args | | `PetnowApiClient` server-API helpers | retired — call the Server API from your app server | | Fragment without a session ID | `ARG_CAPTURE_SESSION_ID` required | | `import io.petnow.ui.PetnowCameraDetectionListener` | `io.petnow.callback.…ListenerV2` (recommended) | | raw `SecurityException` / platform exceptions | sealed `PetnowUIError` | *** ## Migration checklist [#migration-checklist] ### Common [#common] * [ ] Server issues a `captureSessionId` via `POST /v2/capture-sessions` ### iOS [#ios-1] * [ ] `CameraViewModel` → `CameraController`; license into the constructor * [ ] `initializeCamera(initialPosition:captureSessionId:)` with the server session ID * [ ] Replace `previewLayer` usage with `CameraView` (+ `isInitialized` for readiness) * [ ] Call `startDetection()` after `initializeCamera` · `stopDetection()` → `finalizeCamera()` ### Android [#android-1] * [ ] Remove `PetnowApiClient` calls; pass license/config per session; move server-API calls to your app server * [ ] Pass `captureSessionId` (controller arg or `ARG_CAPTURE_SESSION_ID`) * [ ] Listener import to `io.petnow.callback`, move to V2 * [ ] Catch `PetnowUIError` around `initializeCamera()` # SDK v1.3 → v1.4 Migration Source: /petify/docs/sdk/migration/sdk-v1.3-to-v1.4 > A change-by-change guide to upgrading an existing v1.3.x app to v1.4.0. This guide lists the code changes needed to upgrade an existing v1.3.x integration to v1.4.0, per platform. **Coming from v1.2.x?** Skip this page and use the direct [SDK v1.2 → v1.4 Migration](/petify/docs/sdk/migration/sdk-v1.2-to-v1.4) instead — it covers every change in one pass, with no intermediate v1.3-era code. **iOS**: most old public symbols are kept as deprecated aliases, so typical apps still build — with warnings. A handful of v1.3 symbols were removed or hidden without an alias (see *Removed without an alias* below); apps that used them need the listed replacements.
**Android**: the global `PetnowApiClient` entry point (the `apiClient` module) is no longer shipped, so code changes are required.
The v1.4 changes serve two goals — ① **unifying the iOS/Android public API** (`CameraController`, `DetectionConfiguration`/`DetectionPurpose`, `DetectionStatus`/`CameraResult`, etc. now have the same shape on both platforms), and ② **controlling the session lifecycle through clearer APIs** (separate verbs for start/pause/resume/finalize). The **Why** under each item below points to one of these. *** ## iOS [#ios] ### 1. `CameraViewModel` → `CameraController` [#1-cameraviewmodel--cameracontroller] ```swift // v1.3.x (deprecated) @StateObject private var viewModel: CameraViewModel CameraView(viewModel: viewModel) // v1.4.0 @StateObject private var controller: CameraController // [!code highlight] CameraView(controller: controller) // [!code highlight] ``` `CameraViewModel` is a deprecated typealias of `CameraController`. Change `CameraView(viewModel:)` to `CameraView(controller:)` as well. **Why:** This type isn't a mere view model — it's the *controller* that owns the license and detection commands. The rename clarifies the role and aligns the name with Android's `CameraController`. → [Basic Usage](/petify/docs/sdk/ios/basic-usage) ### 2. Move the license to the constructor [#2-move-the-license-to-the-constructor] Passing the license via `initializeCamera(licenseInfo:)` is deprecated. **Inject `licenseInfo` into the constructor** and call `initializeCamera` without the license argument. ```swift // v1.3.x (deprecated) let controller = CameraController(species: .dog, cameraPurpose: .forRegisterFromProfile) try await controller.initializeCamera( licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY"), initialPosition: .back, captureSessionId: sessionId ) // v1.4.0 let controller = CameraController( // [!code highlight] configuration: DetectionConfiguration(species: .dog, purpose: .petProfileRegistration), // [!code highlight] licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY") // [!code highlight] ) try await controller.initializeCamera(initialPosition: .back, captureSessionId: sessionId) // [!code highlight] controller.startDetection() // [!code highlight] ``` `initializeCamera` does **not** start detection — call `startDetection()` explicitly once it returns (same as Android). **Why:** Injecting the license once for the controller's lifetime lets `initializeCamera` focus solely on connecting the camera/session — no re-passing the key on every call, and license validation runs once. (Android works the same way.) → [Getting Started](/petify/docs/sdk/ios/getting-started) ### 3. Teardown: `stopDetection()` → `finalizeCamera()` [#3-teardown-stopdetection--finalizecamera] ```swift // v1.3.x (deprecated) controller.stopDetection() // v1.4.0 controller.finalizeCamera() // [!code highlight] ``` **Why:** The capture/detection session lifecycle is now controlled through clearer APIs. The old `stopDetection()` conflated *stopping detection* with *closing the camera*; v1.4 separates the verbs — start `startDetection()`, pause/resume `pauseDetection()`, full teardown `finalizeCamera()` — so each stage is explicit and you avoid an accidental full teardown. → [Basic Usage](/petify/docs/sdk/ios/basic-usage) ### 4. Removed without an alias [#4-removed-without-an-alias] Most of the surface migrates via deprecated aliases, but these v1.3 members are gone or hidden and need the listed replacement: | Removed in v1.4 | Replacement | | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `captureSession` (raw `AVCaptureSession` getter) | No getter — `CameraView` renders the preview; inject your own session via `CameraController(configuration:licenseInfo:captureSession:)` for a custom preview → [Customization](/petify/docs/sdk/ios/customization) | | `cameraPermissionStatus` | Permission handling is host-owned — check/request `AVCaptureDevice.authorizationStatus(for: .video)` yourself | | `currentCameraPosition` | Internal switch state — drive the UI from your own `switchCamera()` calls | | `LicenseInfo(apiKey:isDebugMode:)` | `isDebugMode` removed — the SDK targets production unconditionally. Use `LicenseInfo(apiKey:)`. Callers passing `isDebugMode:` must drop the argument. | ### iOS change summary [#ios-change-summary] | v1.3.x | v1.4.0 | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `CameraViewModel` | `CameraController` | | `CameraView(viewModel:)` | `CameraView(controller:)` | | `initializeCamera(licenseInfo:…)` | constructor `CameraController(configuration:licenseInfo:)` + `initializeCamera(initialPosition:captureSessionId:)` | | `stopDetection()` | `finalizeCamera()` (detection starts via explicit `startDetection()`) | | `captureSession` / `cameraPermissionStatus` / `currentCameraPosition` | removed/hidden — see *Removed without an alias* | See [Basic Usage](/petify/docs/sdk/ios/basic-usage) for details. *** ## Android [#android] In v1.4 the global `PetnowApiClient` entry point (the `apiClient` module) is **no longer part of the SDK**. The license and detection configuration move to per-session passing, and the module's server-API helpers are gone — code changes are required. ### 1. Global `PetnowApiClient` init/config → per-session passing [#1-global-petnowapiclient-initconfig--per-session-passing] ```kotlin // v1.3.x (the apiClient module is no longer shipped — compile error) PetnowApiClient.init(key = "YOUR_API_KEY", isDebugMode = false) PetnowApiClient.configureDetectionMode( purpose = DetectionPurpose.PET_PROFILE_REGISTRATION, species = PetSpecies.DOG, enableFakeDetection = true ) ``` * **Recommended (`CameraView` + `CameraController`)**: pass `LicenseInfo(apiKey)` to the `CameraController(context, license, scope)` constructor and `DetectionConfiguration` to `initializeCamera(config, captureSessionId)`. → [Basic Usage](/petify/docs/sdk/android/basic-usage) * **Keep the existing `PetnowCameraFragment`**: pass the license via Fragment args (`ARG_API_KEY`) or `provideLicense()`, and the configuration via `ARG_DETECTION_CONFIGURATION`. → [Fragment (legacy)](/petify/docs/sdk/android/fragment) * The configuration types move packages: `io.petnow.api.client.{DetectionConfiguration, DetectionPurpose, PetSpecies}` → `io.petnow.ui.config.*` (same shape). * `PetnowApiClient.isSuccessInitialize` has no replacement — initialization failures now surface as the structured `PetnowUIError` from `initializeCamera()` (item 4). * `isDebugMode` is gone on Android: the SDK targets production unconditionally. **If you called the Petnow Server API through `PetnowApiClient`** (creating capture sessions, uploading fingerprints, registration): those helpers did not move — they were retired with the module. Call the [Server API](/petify/docs/server/api/overview) from **your app server** instead; the v1.4 client SDK handles capture only. **Why:** A global singleton meant the whole app shared one mutable configuration — hard to use different settings per session, manage independent lifecycles, or test. Per-session passing removes the global state, and moving server calls out of the client puts the API key where it belongs (your server). ### 2. Listener import package changed [#2-listener-import-package-changed] ```kotlin // v1.3.x import io.petnow.ui.PetnowCameraDetectionListener // v1.4.0 import io.petnow.callback.PetnowCameraDetectionListener // [!code highlight] // V2: import io.petnow.callback.PetnowCameraDetectionListenerV2 ``` **Why:** The public callback API was simply moved into a dedicated `io.petnow.callback` package. No behavior change — only the import path. → [Basic Usage](/petify/docs/sdk/android/basic-usage) ### 3. (Recommended) Move to the V2 listener [#3-recommended-move-to-the-v2-listener] On the `CameraController` path, use `setDetectionListenerV2` (V2). V2 delivers `DetectionStatus` (sealed: `NoObject`/`Processing`/`Detected`/`Finished`/`Failed(reason)`) and `CameraResult` (`Success(fingerprintImageFiles, appearanceImageFiles)`/`Fail`). **Why:** V1 hands you legacy per-platform models, whereas V2 delivers the same result models as iOS — a type-safe sealed `DetectionStatus` and a `CameraResult` carrying the image file lists. You get cross-platform consistency and richer results. → [Basic Usage](/petify/docs/sdk/android/basic-usage) ### 4. Initialization failures are now the structured `PetnowUIError` [#4-initialization-failures-are-now-the-structured-petnowuierror] In v1.3.x, `initializeCamera()` failures leaked raw platform exceptions (`SecurityException` on a missing permission, camera2 throwables on camera errors). v1.4.0 throws every initialization failure as the sealed class `io.petnow.ui.PetnowUIError`. Also new in v1.4.0: `initializeCamera()` validates the API key against the server (once per key per process), so a rejected key fails with `PetnowUIError.InvalidLicense`. ```kotlin // v1.3.x — catch platform exceptions individually try { controller.initializeCamera(configuration, captureSessionId) } catch (e: SecurityException) { /* permission missing */ } catch (e: Exception) { /* other init errors */ } // v1.4.0 — a single catch + sealed when // [!code highlight] try { controller.initializeCamera(configuration, captureSessionId) } catch (e: PetnowUIError) { // [!code highlight] when (e) { is PetnowUIError.InvalidLicense -> { /* API key rejected */ } is PetnowUIError.PermissionDenied -> { /* permission missing */ } is PetnowUIError.CameraOpenFailed -> { /* camera error — see e.cause */ } } } ``` `CancellationException` still propagates untouched (normal lifecycle — rethrow it). **Why:** one `catch (e: PetnowUIError)` covers every initialization failure, and the sealed `when` makes missing cases a compile-time error. The case names match iOS `PetnowUIError` (`invalidLicense`/`permissionDenied`), so error handling lines up across platforms. The original platform exception is preserved as `cause`. → [Basic Usage — Error Handling](/petify/docs/sdk/android/basic-usage) ### Android change summary [#android-change-summary] | v1.3.x | v1.4.0 | | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | | `PetnowApiClient.init()` | `LicenseInfo` + `CameraController(context, license, scope)` or Fragment args/`provideLicense()` | | `PetnowApiClient.configureDetectionMode()` | `initializeCamera(config, captureSessionId)` or `ARG_DETECTION_CONFIGURATION` | | `PetnowApiClient.isSuccessInitialize` | removed — failures surface as `PetnowUIError` | | `PetnowApiClient` server-API helpers | retired — call the Server API from your app server | | `import io.petnow.ui.PetnowCameraDetectionListener` | `import io.petnow.callback.PetnowCameraDetectionListener` | | `initializeCamera()` failures as `SecurityException` / raw platform exceptions | sealed `PetnowUIError` (`InvalidLicense`/`PermissionDenied`/`CameraOpenFailed`) | See [Basic Usage](/petify/docs/sdk/android/basic-usage) and [Fragment (legacy)](/petify/docs/sdk/android/fragment) for details. *** ## Migration checklist [#migration-checklist] ### iOS [#ios-1] * [ ] `CameraViewModel` → `CameraController`, `CameraView(viewModel:)` → `(controller:)` * [ ] Move the license to the constructor (`CameraController(configuration:licenseInfo:)`); drop the license arg from `initializeCamera` * [ ] `stopDetection()` → `finalizeCamera()`; call `startDetection()` after `initializeCamera` * [ ] Replace uses of `captureSession`/`cameraPermissionStatus`/`currentCameraPosition` (see *Removed without an alias*) ### Android [#android-1] * [ ] Remove `PetnowApiClient` calls → pass license/config per session; move server-API calls to your app server * [ ] Change the listener import to `io.petnow.callback` * [ ] (Recommended) Move to `CameraView` + `CameraController` + the V2 listener * [ ] Replace `SecurityException`/generic catches around `initializeCamera()` with a `PetnowUIError` catch (+ `when`) # SDK v1.4 → v1.5 Migration Source: /petify/docs/sdk/migration/sdk-v1.4-to-v1.5 > What changes when upgrading a v1.4.x integration to v1.5.0 — an Android rebuild, one breaking removal, and a changed default. v1.5.0 moves dog-nose quality assessment onto the device, removes the realtime server quality path introduced in 1.4.2, retires difficulty levels, adds `x86_64` to the Android AAR, and adds an environment selector to `LicenseInfo`. Three things need your attention before you ship: **Android apps must be rebuilt**, the **server-quality surface is gone outright**, and **`enableFakeDetection` now defaults to `true`**. Everything else is source-compatible. 1.5 is **not binary-compatible on Android**. `DetectionConfiguration` gained `inferenceTier` and `LicenseInfo` gained `environment` in their primary constructors, and a Kotlin data class carries its parameter list in the JVM signatures it generates — the constructor, the synthetic default-argument constructor, `copy` and `copy$default` all changed shape. An app compiled against 1.4.5 whose AAR is replaced with 1.5, without the app itself being rebuilt, installs and starts as usual and then throws `NoSuchMethodError` the first time it constructs or copies a configuration. Nothing about that shows up at build time. **Rebuild the app against 1.5. There is nothing to change in its source.** **Coming from v1.3.x?** Do the [v1.3 → v1.4 Migration](/petify/docs/sdk/migration/sdk-v1.3-to-v1.4) first (it has real API changes), then return here. On Android, versions 1.4.1–1.4.4 were never published, so you upgrade from 1.4.0 or 1.4.5 directly to 1.5.0. ## Version bump [#version-bump] | Platform | v1.4.x | v1.5.0 | | ---------------------- | -------------------------------------- | -------------------------------------- | | Android | `io.petnow:ui:1.4.5` | `io.petnow:ui:1.5.0` | | iOS (SwiftPM registry) | `petnow.ui` 1.4.5 | `petnow.ui` 1.5.0 | | React Native | `@petify/react-native-camera-ui@1.4.5` | `@petify/react-native-camera-ui@1.5.0` | Package size grows because the on-device quality model ships inside the SDK: the Android AAR is about 74 MB with both ABIs (the 12 MB model is stored once; App Bundles carry it once), and the iOS framework grows by about 12 MB. *** ## Breaking: the realtime server-quality surface is gone [#breaking-the-realtime-server-quality-surface-is-gone] If you took part in the 1.4.2–1.4.5 realtime server quality pilot, this is a **compile error**, not a cleanup you can defer. There is no deprecated no-op left behind. | Platform | Remove | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | React Native | the `onRealtimeServerIqaDiagnostic` prop and every use of the `RealtimeServerIqaDiagnosticEvent` / `RealtimeServerIqaFeatureScores` / `RealtimeServerIqaDiagnosticPhase` types | | Android | `PETNOW_SERVER_IQA_*` `BuildConfig` fields / Gradle properties | | iOS | `PetnowServerIqa*` keys in `Info.plist`, and any `setRealtimeServerIqaDiagnosticHandler` call | Delete the prop and the types — nothing replaces them. The callback reported a server judgement that no longer happens, and dog quality scores are not part of the public API. Integrations that never enabled the pilot have nothing to do here. Capture no longer waits on a server call. License validation runs in `initializeCamera()`, once per API key per process; an unreachable server at that moment fails with `InvalidLicense`. After that, detection and the returned files are local, and the usage metrics the SDK reports in the background never block a capture or make one fail. If your app handled "session stuck at `detected`" or server-quality timeouts specially, that handling can go. → [How capture quality is decided](/petify/docs/overview#how-capture-quality-is-decided) *** ## Changed default: `enableFakeDetection` is `true` [#changed-default-enablefakedetection-is-true] `DetectionConfiguration.enableFakeDetection` defaults to **`true`** on every surface (iOS, Android, React Native). In 1.4.5 an integrator who never set the flag got no spoof detection; on 1.5 the same code gets it. **Check your own call sites before upgrading** — a flow tested against photographs of pets will start failing. Passing `false` turns spoof detection off for that session, which is how you evaluate the SDK against photographs instead of live animals. ```kotlin // 1.4.5 — off unless you asked for it DetectionConfiguration(species = PetSpecies.DOG, purpose = DetectionPurpose.PET_PROFILE_REGISTRATION) // 1.5.0 — the same call now has spoof detection on // 1.5.0 — testing against photographs DetectionConfiguration( species = PetSpecies.DOG, purpose = DetectionPurpose.PET_PROFILE_REGISTRATION, enableFakeDetection = false, // [!code highlight] ) ``` *** ## New: `LicenseInfo.environment` [#new-licenseinfoenvironment] A staging-console API key is only valid against the staging backend, and vice versa. v1.5 lets the host name the backend its key belongs to, restoring the capability v1.3.x had as `isDebugMode` and v1.4 removed. ```kotlin // Android LicenseInfo(apiKey = "YOUR_API_KEY", environment = PetnowEnvironment.STAGING) ``` ```swift // iOS LicenseInfo(apiKey: "YOUR_API_KEY", environment: .staging) ``` ```tsx // React Native const camera = usePetnowCamera({ apiKey, environment: 'staging' }); ``` Omitting it keeps the environment the SDK was built for, which is **production** for every published build — nothing changes for an app that does not set it. It is a named environment rather than a boolean on purpose: do not wire it to your own `DEBUG` / `__DEV__` flag, because a release build that accidentally selected staging would send real captures to a backend where the pets are not registered. *** ## New: one API key per process [#new-one-api-key-per-process] A second, **different** API key in the same process is now refused instead of silently replacing the first. The console can issue several keys; one process must pick one of them and keep it. | Platform | Error | | ------------ | ------------------------------------------------------------------------------- | | Android | `PetnowUIError.MultipleApiKeys` from `initializeCamera()` | | iOS | `PetnowUIError.multipleApiKeys` | | React Native | give `usePetnowCamera` one stable `apiKey`; switching keys needs an app restart | The first key keeps working. Re-initializing with the *same* key is unaffected. *** ## Deprecated: `difficultyMode` [#deprecated-difficultymode] Still accepted, still ignored, and it stays that way through 1.5 — it goes in the next major release. Remove the argument at your convenience to silence the deprecation warning. ```kotlin // v1.4.x DetectionConfiguration( species = PetSpecies.DOG, purpose = DetectionPurpose.PET_PROFILE_REGISTRATION, enableFakeDetection = true, difficultyMode = DifficultyMode.NORMAL, // deprecated in 1.5.0 — ignored ) // v1.5.0 DetectionConfiguration( species = PetSpecies.DOG, purpose = DetectionPurpose.PET_PROFILE_REGISTRATION, enableFakeDetection = true, ) ``` The same applies to iOS `DetectionConfiguration(difficultyMode:)` and the React Native `` prop. **Why:** dog-nose capture quality is judged by the on-device model, and difficulty levels no longer take part in that judgement. → [How capture quality is decided](/petify/docs/overview#how-capture-quality-is-decided) *** ## Android [#android] ### 1. Dependencies — a single self-contained AAR [#1-dependencies--a-single-self-contained-aar] ```kotlin dependencies { implementation("io.petnow:ui:1.5.0") // remove if still present: // implementation("io.petnow:api-client:...") // implementation(files("AndroidCameraFramework-release.aar")) } ``` `io.petnow:ui` bundles everything it needs. The `api-client` module and the `AndroidCameraFramework` artifact are not published for 1.4/1.5 and must not be declared. Server calls belong on your app server → [Server API](/petify/docs/server/api/overview). ### 2. ABIs — `x86_64` added, `armeabi-v7a` not shipped [#2-abis--x86_64-added-armeabi-v7a-not-shipped] The AAR contains `arm64-v8a` and `x86_64`. Emulator-based tests now run the SDK (GPU acceleration on an emulator depends on its GL/OpenCL support and falls back to CPU). If your app declares `abiFilters`, make sure `armeabi-v7a`-only builds are not expected to include the SDK. ### 3. Optional: `inferenceTier` and `performanceStats` [#3-optional-inferencetier-and-performancestats] * `DetectionConfiguration.inferenceTier` (`AUTO` default / `CPU` / `GPU`) selects the detector path. `AUTO` picks the path that suits the device, and falls back if that path is unavailable. It is read **once**, when the process creates its first detector engine, and fixed from then on — set it on the first `initializeCamera()` or restart the app to change it. Force a tier only when support asks you to. * `CameraController.performanceStats` (`StateFlow`) and `PetnowCameraFragment.performanceStats()` report camera init time, first-frame time, fps, detector ms and the chosen tier — useful in a diagnostics screen or a support report. → [Basic Usage](/petify/docs/sdk/android/basic-usage#performance-stats-optional) ### 4. Legacy Fragment path — result lists per species [#4-legacy-fragment-path--result-lists-per-species] If you still use `PetnowCameraFragment` with the V1 `DetectionCaptureResult`: for a **dog** both `noseImageFiles` (biometric set → upload as fingerprints) and `faceImageFiles` (appearance set) are populated; for a **cat** only `faceImageFiles` is populated and it is the biometric set. This is unchanged from 1.4 but was previously undocumented. *** ## iOS [#ios] * `CameraController.performanceStats` (`@Published CameraPerformanceStats`) is new; nothing to change. * `CameraOpticsPolicy` and the adaptive rear-camera mode are gone — they existed only for the server-quality path. Rear lens selection behaves as in 1.4.3. * `setRealtimeServerIqaDiagnosticHandler` and the `RealtimeServerIqa*` types are gone (see *Breaking* above). *** ## React Native [#react-native] * `onRealtimeServerIqaDiagnostic` and the `RealtimeServerIqa*` types are removed (see *Breaking* above); `difficultyMode` is deprecated and never reaches native. Everything else in `usePetnowCamera` / `` is unchanged. * Supported range is unchanged: React Native 0.75 or newer, tested through 0.86.x, with the New Architecture enabled. * A new example app is available for 1.5.0; contact [support@petnow.io](mailto:support@petnow.io) for it. *** ## Migration checklist [#migration-checklist] * [ ] Bump the package version (`io.petnow:ui:1.5.0` / `petnow.ui` 1.5.0 / `@petify/react-native-camera-ui@1.5.0`) * [ ] **Android: rebuild the app.** Do not ship 1.5's AAR inside a binary compiled against 1.4.5 * [ ] Pilot integrations only: delete `onRealtimeServerIqaDiagnostic`, the `RealtimeServerIqa*` types, and the `PETNOW_SERVER_IQA_*` / `Info.plist` / `.env` configuration * [ ] Review every `DetectionConfiguration` call site for the new `enableFakeDetection` default * [ ] Confirm one API key per process, and that a staging key is paired with `environment` = staging * [ ] Remove `difficultyMode` arguments (optional, silences the deprecation warning) * [ ] Android: remove `api-client` / `AndroidCameraFramework` declarations if present * [ ] Re-run your capture flow on a device; nothing in the callbacks or result files changed # Basic Usage Source: /petify/docs/sdk/react-native/basic-usage > A step-by-step guide to integrating the camera UI with the usePetnowCamera hook and CameraView. *** ## Before You Begin [#before-you-begin] Complete [Getting Started](/petify/docs/sdk/react-native/getting-started) before following this guide. You need the package installed, camera permissions configured, and a Petnow API key. This guide walks you step by step through integrating pet nose-print/face capture into your app using the `usePetnowCamera` hook and ``.
### Prepare the Camera [#prepare-the-camera] Create the controller with `usePetnowCamera` and wait for the `ready` state. ### Display the Camera [#display-the-camera] Once `ready`, mount `` to show the preview and overlay. ### Receive Events [#receive-events] Handle detection status, progress, and the finished result through callbacks. ### Handle the Result [#handle-the-result] Upload the captured images to your app server. ### Additional Features [#additional-features] Use retake, camera switching, and pause/resume.
*** ## Step 1: Prepare the Camera [#step-1-prepare-the-camera] `usePetnowCamera` is the hook that holds the license and returns a **controller handle** exposing the detection commands. It stores the `apiKey` in the native SDK (and stores it again when `apiKey` changes), and exposes the SDK readiness state through `camera.state`. **Wait until `status` becomes `'ready'`** before mounting ``, and it is recommended to display a loading screen until then. ```tsx import { usePetnowCamera, CameraView } from '@petify/react-native-camera-ui'; function Scan({ apiKey }) { const camera = usePetnowCamera({ apiKey }); // Mount CameraView only when ready. // [!code highlight] if (camera.state.status !== 'ready') { // [!code highlight] return ; // your own loading component } // Mount in Step 2. return null; } ``` ### camera.state [#camerastate] The SDK readiness state. **Mount `` only when `status === 'ready'`.** | status | Meaning | | -------------- | -------------------------------------------------------------- | | `idle` | `apiKey` is not set yet | | `initializing` | Storing the license in the native SDK | | `ready` | Storage complete — the camera can be mounted | | `error` | The native `initialize` call failed (message in `state.error`) | `state` tracks **license storage**, **not validation**. License validation is performed when the camera view mounts (`initializeCamera`) on both platforms: a rejected key surfaces as `failed` in `onDetectionStatus`, and a key that has already been validated is not re-validated within the same process. Mounting `` before `ready` is not supported. *** ## Step 2: Display the Camera [#step-2-display-the-camera] Once `camera.state` becomes `ready`, mount ``. **Mounting initializes the camera, and unmounting cleans it up.** Pass the species (`species`), purpose (`purpose`), and the server-issued `captureSessionId` as props. ```tsx ``` For how to layer UI such as guide text and buttons on top of ``, see [Customization](/petify/docs/sdk/react-native/customization). ### Props [#props] | Prop | Type | Description | | ---------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `camera` | `PetnowCamera` | The controller returned by `usePetnowCamera` | | `species` | `'DOG' \| 'CAT'` | Pet species (determines the detection pipeline) | | `purpose` | `'PET_PROFILE_REGISTRATION' \| 'PET_IDENTIFICATION' \| 'PET_VERIFICATION'` | Capture purpose (determines the number of required images) | | `captureSessionId` | `string` | Server-issued UUID ([Server API](/petify/docs/server/api/overview)) | | `difficultyMode?` | `'EASY' \| 'NORMAL' \| 'HARD'` | Deprecated since 1.5.0 — accepted and ignored | | `enableFakeDetection?` | `boolean` | Spoof detection for photos, screens and printed images (default `true`) | | `bracketingMode?` | `boolean` | Capture bracketing — briefly burst-shoots around the adopted frame to collect a better fingerprint image (default `false`). Can be toggled at runtime (no remount needed) | | `showDetectionMarker?` | `boolean` | Show the SDK-provided nose/face marker (default `true`). Set `false` to draw a fully custom overlay from `onDetectionResult` rects — toggling at runtime does not restart the capture or detection session (SDK 1.4.3+) | | `style` | `ViewStyle` | Standard RN view style | Spoof detection (photos, screens, printed images) is on by default. Passing `false` turns spoof detection off for that session, which is how you evaluate the SDK against photographs of pets instead of live animals. Changing the session settings (`species`/`purpose`/`enableFakeDetection`) or `captureSessionId` automatically triggers a **re-initialization flow** in the native side on the same view — changing `species`/`purpose`/`enableFakeDetection` recreates the controller, and changing `captureSessionId` reconfigures the capture runtime and detector. If you want a clearer transition in terms of UX, you can also re-mount the view with a React `key`. The server capture session's `petId` requirement varies depending on the capture purpose (`purpose`) — registration and verification require a `petId`, while identification does not. For details, see [Server API – Biometric Data](/petify/docs/server/api/biometric). *** ## Step 3: Receive Events [#step-3-receive-events] Detection status, progress, and the finished result are delivered through ``'s event callbacks. ```tsx onDetectionStatus={(s: DetectionStatus) => { /* {type} or {type:'failed', reason} */ }} onDetectionProgress={(p: number) => { /* 0 ~ 100 */ }} onDetectionFinished={(r: CameraResult) => { /* {success, fingerprintImages, appearanceImages} */ }} onDetectionResult={(r: DetectionResult) => { /* per-frame nose/face boxes — for custom markers */ }} ``` * `onDetectionStatus`: the current detection status (`DetectionStatus`). `type` is `noObject` | `processing` | `detected` | `finished` | `failed` (with `reason` in this case). * `onDetectionProgress`: progress as an integer from `0` to `100`. * `onDetectionFinished`: the final result (`CameraResult`) — `fingerprintImages` / `appearanceImages` are arrays of local `file://` URIs. * `onDetectionResult`: per-frame nose/face detection boxes (`DetectionResult`). Use it to draw markers yourself; for an example, see [Customization](/petify/docs/sdk/react-native/customization#example-detection-rect-tracking-overlay). For the definition of each type, see [Types](#types) below. `onDetectionProgress` and `onDetectionResult` are called **on every detection update (close to frame rate) on the main thread**. Keep these handlers lightweight and avoid heavy work or excessive `setState`. *** ## Step 4: Handle the Result / Upload [#step-4-handle-the-result--upload] The `fingerprintImages` / `appearanceImages` from `onDetectionFinished` are arrays of local `file://` URIs. Upload them from JS **to your app server**, then the server performs registration, verification, and identification via the [Server API](/petify/docs/server/api/biometric). The client only handles capture and upload. Even when capture fails, `onDetectionFinished` is still called with `success: false` and **empty image arrays** (the failure reason is delivered separately via `onDetectionStatus` `failed`). Check `r.success` before using the result. Keep the key your server calls the Petnow API with (`x-petnow-api-key`) **on your app server**. Do not call the Petnow API directly from the RN client — send the `file://` URIs to your app server as shown below, and have the server proxy them to [`/v2/fingerprints:upload` / `/v2/appearances:upload`](/petify/docs/server/api/biometric). This is a different key from the one you pass to `usePetnowCamera` → [API Key Security](/petify/docs/sdk/react-native/getting-started#api-key-security). In React Native, upload a `file://` URI via `FormData`: ```tsx async function upload(r: CameraResult) { if (!r.success) return; const form = new FormData(); // RN's FormData accepts files as { uri, name, type }. r.fingerprintImages.forEach((uri, i) => form.append('fingerprints', { uri, name: `nose_${i}.jpg`, type: 'image/jpeg' } as any), ); r.appearanceImages.forEach((uri, i) => form.append('appearances', { uri, name: `face_${i}.jpg`, type: 'image/jpeg' } as any), ); form.append('captureSessionId', captureSessionId); // Send to your own app-server endpoint → the server uploads to Petnow with x-petnow-api-key await fetch('https://your-app-server.example.com/petnow/upload', { method: 'POST', body: form }); } ``` *** ## Step 5: Additional Features [#step-5-additional-features] These are the commands for controlling the camera during and after capture. Commands work after the bound `` has mounted. | Method | Description | | -------------------------- | -------------------------------------------------------------------------------------------- | | `camera.startDetection()` | Start a detection session (or restart from the beginning — retake) | | `camera.pauseDetection()` | Pause detection (keeps the camera, preserves progress) | | `camera.resumeDetection()` | Resume paused detection | | `camera.switchCamera()` | Switch between front and rear cameras | | `camera.retry()` | Retry native initialization with the same `apiKey` (for recovering from a transient `error`) | ### Retake / Continuous Capture [#retake--continuous-capture] After `onDetectionFinished`, calling `camera.startDetection()` on the same view detects again from the beginning (retake). For a continuous flow, simply restart in the completion callback. ```tsx const onFinished = useCallback((r: CameraResult) => { upload(r); if (continuousMode) { setTimeout(() => camera.startDetection(), 1200); // restart after a short pause } }, [camera, continuousMode]); ``` *** ## Types [#types] These are the types used by `usePetnowCamera` and ``. `LicenseInfo.environment` is optional: omit it to keep the environment the SDK was built for, which is **production** for every published build. A staging-console key is only valid against the staging backend and vice versa — a mismatch fails license validation. Do not wire it to `__DEV__`; a release build that accidentally selected staging would send real captures to a backend where the pets are not registered. (This restores the capability v1.3.x had as `isDebugMode`, removed in v1.4.) Pass one stable `apiKey` for the life of the process — switching to a different key requires an app restart. ```ts // usePetnowCamera options type PetnowEnvironment = 'production' | 'staging'; type LicenseInfo = { apiKey: string; environment?: PetnowEnvironment; }; // The controller handle returned by usePetnowCamera type PetnowCamera = { state: PetnowCameraState; startDetection(): void; // Start detection / retake pauseDetection(): void; // Pause (preserves progress) resumeDetection(): void; // Resume switchCamera(): void; // Switch front/rear retry(): void; // Retry native initialization }; type PetnowCameraState = | { status: 'idle' } | { status: 'initializing' } | { status: 'ready' } | { status: 'error'; error: string }; // error message only on the 'error' state // onDetectionStatus callback argument type DetectionStatus = | { type: 'noObject' } // No target detected | { type: 'processing' } // Detection in progress | { type: 'detected' } // Detection successful | { type: 'finished' } // Capture complete | { type: 'failed'; reason: DetectionFailureReason; rawReason?: string }; // failed: `reason` is the normalized value. When a newer native SDK reports a // reason this JS build does not know, `reason` is 'Unexpected' and the // original string is kept in `rawReason`. // onDetectionFinished callback argument type CameraResult = { success: boolean; fingerprintImages: string[]; // local file:// URIs appearanceImages: string[]; // local file:// URIs }; // onDetectionResult callback argument (per-frame detection boxes) type BoundingBox = { x: number; y: number; width: number; height: number }; // normalized 0–1 type DetectionResult = { nose: BoundingBox | null; face: BoundingBox | null; }; ``` ## Error / Permission Handling [#error--permission-handling] There is no separate error channel. Failures surface in two places. * **Initialization (license storage) failure** → `camera.state` becomes `error`. Retry with `camera.retry()`. * **Camera/detection stage failure** (rejected license key, permission denied, camera open failure, capture failure) → `{ type: 'failed', reason }` in `onDetectionStatus`. ```tsx function guideMessage(status: DetectionStatus | null): string { if (!status) return 'Initializing camera...'; switch (status.type) { case 'failed': return `Recognition failed: ${status.reason}`; case 'noObject': return 'Center your pet on the screen'; case 'finished': return 'Capture complete!'; default: return ''; } } ``` When permission is denied, `failed` is delivered. Guide the user to grant the permission in Settings, then re-mount ``. ## Two-Tier Lifecycle [#two-tier-lifecycle] The camera (view lifetime) is the outer layer, and detection is a sub-lifecycle that runs within it. For the diagram and a detailed explanation, see [Introduction](/petify/docs/sdk/react-native/intro#two-tier-lifecycle). ## Automatic Session Lifetime [#automatic-session-lifetime] The camera is a single hardware resource, but RN views are frequently destroyed and recreated, so the session is managed by the package as a singleton rather than by the view. With a 1.5-second grace period after the last detach, the session is reused across transitions and remounts, and when `captureSessionId` changes, the capture runtime and detector are reconfigured with the new session ID while keeping the controller/session ownership structure intact. For details, see [Introduction](/petify/docs/sdk/react-native/intro). ## Attention Sounds [#attention-sounds] You can play attention sounds (`PetnowSound`) to draw a pet's attention **independently** of the camera. For the full sound list, previews, and API, see the [Sound Guide](/petify/docs/sdk/react-native/sound). ## Complete Code [#complete-code] This example combines the hook, the `ready` gate, ``, events, and upload into a single component. The `captureSessionId` is received as a prop, issued by your app server via the [Server API](/petify/docs/server/api/overview). ```tsx import { useCallback, useState } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { usePetnowCamera, CameraView, type DetectionStatus, type CameraResult, } from '@petify/react-native-camera-ui'; export function Scan({ apiKey, captureSessionId }: { apiKey: string; captureSessionId: string }) { const camera = usePetnowCamera({ apiKey }); const [, setStatus] = useState(null); // Upload file:// URIs to your app server (which proxies to Petnow with x-petnow-api-key) const upload = useCallback( async (r: CameraResult) => { if (!r.success) return; const form = new FormData(); r.fingerprintImages.forEach((uri, i) => form.append('fingerprints', { uri, name: `nose_${i}.jpg`, type: 'image/jpeg' } as any), ); r.appearanceImages.forEach((uri, i) => form.append('appearances', { uri, name: `face_${i}.jpg`, type: 'image/jpeg' } as any), ); form.append('captureSessionId', captureSessionId); await fetch('https://your-app-server.example.com/petnow/upload', { method: 'POST', body: form }); }, [captureSessionId], ); // Do not mount CameraView before ready. if (camera.state.status !== 'ready') { return ( Preparing camera… ({camera.state.status}) ); } return ( ); } const styles = StyleSheet.create({ fill: { flex: 1 }, center: { flex: 1, alignItems: 'center', justifyContent: 'center' }, }); ``` ## Next Steps [#next-steps] * [Customization](/petify/docs/sdk/react-native/customization) — Layering guide, buttons, and result UI on top of `` * [Sound Guide](/petify/docs/sdk/react-native/sound) — Playing attention sounds # Customization Source: /petify/docs/sdk/react-native/customization > How to layer your own RN UI on top of CameraView to build guide, button, and result screens. *** Complete [Basic Usage](/petify/docs/sdk/react-native/basic-usage) before starting this guide. ## Overview [#overview] `` renders the camera preview and the SDK detection overlay (nose/face markers, progress ring) **natively**. On top of it, you build the **app UI** — such as guide messages, buttons, progress, and result screens — yourself by **overlaying ordinary RN views with absolute positioning**. The UI is driven by the `onDetectionStatus` / `onDetectionProgress` / `onDetectionFinished` events and the `camera` commands. The detection markers (nose/face) are drawn by the SDK itself. In addition, the **detection rect (nose/face normalized box) is exposed to JS via `onDetectionResult`**, so you can also build custom guides in RN that follow the marker positions ([example below](#example-detection-rect-tracking-overlay)). ## Basic Pattern [#basic-pattern] Lay `` over the screen and overlay an overlay container on top of it. Use `pointerEvents="box-none"` so that touches pass through to the camera area as well. ```tsx import { View, StyleSheet } from 'react-native'; {/* App UI overlay */} {/* ...guide / buttons / progress... */} ``` ## Example: Top Buttons (Switch / Close) [#example-top-buttons-switch--close] ```tsx camera.switchCamera()}> Switch Close ``` ## Example: Status-Based Guide Messages [#example-status-based-guide-messages] Receive `onDetectionStatus` as state and render user guidance text. ```tsx const [status, setStatus] = useState(null); function guideMessage(s: DetectionStatus | null): string { if (!s) return 'Preparing camera...'; switch (s.type) { case 'noObject': return 'Center your pet on the screen'; case 'processing': return 'Recognizing...'; case 'detected': return 'Great! Hold still'; case 'finished': return 'Capture complete!'; case 'failed': return `Try again: ${s.reason}`; } } {guideMessage(status)} ``` ## Example: Progress [#example-progress] ```tsx const [progress, setProgress] = useState(0); // onDetectionProgress (0~100) ``` ## Example: Result Screen and Retake [#example-result-screen-and-retake] When you receive completion via `onDetectionFinished`, show the result/upload UI, and restart with `camera.startDetection()` on "Retake". ```tsx const [result, setResult] = useState(null); const onFinished = useCallback((r: CameraResult) => { setResult(r); upload(r); // upload the file:// URIs to the app server }, []); const retake = useCallback(() => { setResult(null); setStatus(null); setProgress(0); camera.startDetection(); }, [camera]); {result && ( Capture complete — {result.fingerprintImages.length} fingerprint images Retake )} ``` ## Example: Detection Rect Tracking Overlay [#example-detection-rect-tracking-overlay] With `onDetectionResult`, you receive the nose/face box (normalized 0–1, top-left origin), and you can draw a custom marker that follows that position yourself. Obtain the view size with `onLayout` to convert the normalized coordinates to pixels. ```tsx import { useState } from 'react'; import { View, StyleSheet } from 'react-native'; import type { DetectionResult } from '@petify/react-native-camera-ui'; const [rect, setRect] = useState(null); const [size, setSize] = useState({ w: 0, h: 0 }); setSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height }) } > setRect(r.nose ?? r.face)} /> {rect && ( )} ``` * `nose`/`face` are each `{ x, y, width, height }` (normalized 0–1) or `null`. They can be `null` on frames with no detection, so guard for it. * Since it is drawn separately from the SDK's built-in markers, you can omit this overlay if the built-in markers are sufficient. ## Tips [#tips] * Use `pointerEvents="box-none"` on the overlay container and areas that don't need taps, and keep default behavior only on interactive elements like buttons. * If you need to pause (e.g., showing a sheet/popup), you can call `camera.pauseDetection()` and then resume with `camera.resumeDetection()` while preserving progress. * Feel free to change the guide text and failure-reason mapping to match your app's tone. ## Next Steps [#next-steps] * [Basic Usage](/petify/docs/sdk/react-native/basic-usage) — hook, event, and command reference # Getting Started Source: /petify/docs/sdk/react-native/getting-started > Installation and initial setup guide for the Petnow React Native camera UI package. ## Overview [#overview] This guide walks you through installing and configuring `@petify/react-native-camera-ui` in a React Native app, step by step. ## Prerequisites [#prerequisites] * React Native 0.75 or newer, tested through 0.86.x, with **New Architecture (Fabric/TurboModules) enabled** * Android minSdk 28 or higher / iOS 16.4 or higher * **AWS CLI** (required for CodeArtifact authentication — install below) * Petnow API key — issued from the [Petify Console](/petify/docs/petify-console) (or contact [support@petnow.io](mailto:support@petnow.io)) **Expo support**: Currently this package targets a **bare React Native** environment with New Architecture enabled. **Official Expo (managed) support is planned.** ## Step 1: Installation [#step-1-installation] This package is distributed via **AWS CodeArtifact (npm registry)**. It uses the same credential scheme as the Android SDK's [CodeArtifact setup](/petify/docs/sdk/android/getting-started#step-1-install-the-sdk). ### Install the AWS CLI [#install-the-aws-cli] CodeArtifact authentication requires the AWS CLI. Install it from the following link: [https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) ### CodeArtifact Authentication and Installation [#codeartifact-authentication-and-installation] The distribution credentials — Access Key ID / Secret Access Key / Region / domain / account ID (domain-owner) / repository — are issued from the My Page section of the [Petify Console](/petify/docs/petify-console) (or contact [support@petnow.io](mailto:support@petnow.io)). For the console sign-up and key issuance procedure, see the [Petify Console](/petify/docs/petify-console) documentation. Set the environment variables with the values issued from the Petify Console, then log npm into CodeArtifact (configures `~/.npmrc`): ```bash export AWS_ACCESS_KEY_ID= export AWS_SECRET_ACCESS_KEY= export AWS_DEFAULT_REGION= export PETNOW_CODEARTIFACT_DOMAIN= export PETNOW_AWS_ACCOUNT_ID= export PETNOW_NPM_REPOSITORY= aws codeartifact login --tool npm \ --domain "$PETNOW_CODEARTIFACT_DOMAIN" \ --domain-owner "$PETNOW_AWS_ACCOUNT_ID" \ --repository "$PETNOW_NPM_REPOSITORY" \ --region "$AWS_DEFAULT_REGION" ``` The repository name (`PETNOW_NPM_REPOSITORY`) is shown in the Petify Console under **My Page → AWS CodeArtifact credentials**; copy it from there, because the npm registry root returns `404` to listing requests, so the value cannot be discovered from the registry itself. > **Token expiration**: CodeArtifact authentication tokens expire every 12 hours. If `npm install` fails with an authentication error, run `aws codeartifact login` again. Then install the package: ```bash npm install @petify/react-native-camera-ui # or yarn add @petify/react-native-camera-ui ``` The package includes the native binaries (iOS `PetnowUI.xcframework`, Android AAR), so no separate native SDK installation is required. ### iOS [#ios] ```bash cd ios && pod install ``` ### New Architecture [#new-architecture] New Architecture must be enabled in `gradle.properties` (Android) and the environment (iOS). The default RN 0.76+ template has it enabled. ## Step 2: Permission Setup [#step-2-permission-setup] ### Android [#android] Declare the permissions in `AndroidManifest.xml`. The runtime permission request is performed automatically by the package when the camera mounts. ```xml ``` ### iOS [#ios-1] Add a camera usage description to `Info.plist`. ```xml NSCameraUsageDescription The camera is used to photograph pets. ``` ## Step 3: License Initialization [#step-3-license-initialization] There is no global initialization function. The `usePetnowCamera({ apiKey, environment })` hook ties the controller's lifetime to a React component and holds the license. `environment` is optional. ```tsx import { usePetnowCamera, CameraView } from '@petify/react-native-camera-ui'; function Scan() { const camera = usePetnowCamera({ apiKey: 'YOUR_API_KEY' }); // Do not mount CameraView before it is ready. if (camera.state.status !== 'ready') { return ; } return ; } ``` **Mount `` only when `camera.state.status === 'ready'`.** `state` means the SDK is ready (license storage complete). License *validation* happens when the camera view mounts, on **both** platforms: a rejected key surfaces as `failed` in `onDetectionStatus`, and a key that has already been validated is not re-validated within the same process. For details on the behavior, see [Basic Usage](/petify/docs/sdk/react-native/basic-usage). **API key exposure**: The SDK embeds `apiKey` in the app binary and sends it as `x-petnow-api-key` (the same key type as the Server API) for its internal license/monitoring calls. Because this key shares scope with server operations, a key extracted from the app can call your account's Server API. Use a dedicated key for the app and keep your server key out of the binary — see [API Key Security](#api-key-security) below. Give `usePetnowCamera` **one stable `apiKey`** for the life of the process. The console can issue several keys, but one process must use one of them — switching to a different key requires an app restart. ### Backend Environment (`environment`) [#backend-environment-environment] ```tsx // environment?: 'production' | 'staging' const camera = usePetnowCamera({ apiKey: 'YOUR_STAGING_API_KEY', environment: 'staging', }); ``` Omit `environment` to keep the environment the SDK was built for, which is **production** for every published build. Nothing changes for an app that does not set it. A key issued by the staging console is only valid against the staging backend and vice versa — a mismatch fails license validation. `environment` is a named environment on purpose: do **not** wire it to `__DEV__`. A release build that accidentally selected staging would send real captures to a backend where the pets are not registered. (This restores the capability v1.3.x had as `isDebugMode`, which was removed in v1.4.) ## Step 4: Create a Capture Session [#step-4-create-a-capture-session] `captureSessionId` is a UUID issued on the server via the [Petnow Server API](/petify/docs/server/api/overview). The client does not create it. ```tsx // Create a capture session on the server (or via your app server) → receive captureSessionId (UUID) const captureSessionId = await yourServerApi.createCaptureSession({ species: 'DOG', purpose: 'PET_PROFILE_REGISTRATION', }); ``` Creating the capture session is **the responsibility of the app server** (the client only uses the issued `captureSessionId`). The `petId` requirement differs by purpose (required for registration and verification, not needed for identification). For the **full server flow** — from creation parameters and `petId` requirements to uploading the result images — see [Server API – Biometric Data](/petify/docs/server/api/biometric). ## Step 5: Verify the installation [#step-5-verify-the-installation] To confirm the SDK is installed correctly, build and run your app. Once `camera.state.status` becomes `'ready'`, `` mounts and the **camera preview and detection overlay (nose/face markers)** are shown. You can confirm that state and events are coming through by logging them: ```tsx const camera = usePetnowCamera({ apiKey }); console.log('state:', camera.state.status); // idle → initializing → ready return ( console.log('detection:', s.type)} // noObject / processing / ... /> ); ``` If the preview appears, `state` reaches `ready`, and `onDetectionStatus` is called, everything is working. If the screen is black or no events arrive, see [Troubleshooting](#troubleshooting) below. ## Troubleshooting [#troubleshooting] ### New Architecture Not Enabled [#new-architecture-not-enabled] **Symptom**: The `PetnowCamera` native module/view cannot be found, or codegen-related build errors. **Solution**: This package is New Architecture (Fabric/TurboModules) only. The default RN 0.76+ template has it enabled. On older versions, verify `newArchEnabled=true` (Android `gradle.properties`) and iOS `RCT_NEW_ARCH_ENABLED=1`, then run `pod install`. ### npm install Fails with an Authentication Error (401/403) [#npm-install-fails-with-an-authentication-error-401403] **Solution**: CodeArtifact tokens expire every 12 hours. Run `aws codeartifact login --tool npm ...` again, then reinstall. Verify that the domain, account, and repository values, and your AWS credentials, match the values issued by the Petify Console. ### Build/Link Failure on iOS [#buildlink-failure-on-ios] **Solution**: Make sure you ran `cd ios && pod install`. The package includes `PetnowUI.xcframework` as a vendored framework. ### Camera Shows a Black Screen or Permission Is Denied [#camera-shows-a-black-screen-or-permission-is-denied] **Solution**: Verify that the camera permission is declared in the manifest/Info.plist. The package requests the runtime permission, and if the user denies it, `failed` is delivered via `onDetectionStatus`. Guide the user to grant the permission in Settings, then re-mount ``. ### Camera Doesn't Appear Even Though `ready` [#camera-doesnt-appear-even-though-ready] **Solution**: Verify that you did not mount `` before `camera.state.status === 'ready'` (an `initialize() must complete...` warning appears in the logs). Just respect the `ready` gating. ## API Key Security [#api-key-security] The key you pass to `usePetnowCamera({ apiKey })` is the **same Petnow API key type** used for `x-petnow-api-key` on the [Server API](/petify/docs/server/api/overview). Treat it as a credential: anything shipped inside an app binary can be read back out of it. **Recommended setup** * Issue a **dedicated key for the mobile app**, separate from the key your server uses. An account can hold multiple active keys. * Do not embed the server key in the app. Where possible, fetch the app key from your own server at runtime instead of hardcoding it. **Revocation** * Keys are deactivated in the [Petify Console](/petify/docs/petify-console) (My Page → API Key Management). * Deactivation takes effect immediately and does not affect other keys. * A deactivated key cannot be reactivated — create a new one instead. **Zero-downtime rotation on an app release** 1. Create a new app key. 2. Ship the new app version with the new key. 3. Let older app versions drain. 4. Deactivate the old key. There is no automatic key rotation inside the SDK. The CodeArtifact / AWS access keys from Step 1 are used only to download the SDK at build time. They are unrelated to runtime authentication. ## Next Steps [#next-steps] Installation and setup are complete! You're now ready to use the SDK. * [Basic Usage](/petify/docs/sdk/react-native/basic-usage) — `usePetnowCamera` + `CameraView`, events, commands, and lifecycle * [Customization](/petify/docs/sdk/react-native/customization) — Layering UI on top of `` ## Support [#support] If you run into problems during installation, contact [support@petnow.io](mailto:support@petnow.io). # React Native SDK Introduction Source: /petify/docs/sdk/react-native/intro > Introduces the structure and technical requirements of the Petnow React Native camera UI package. ## Latest Version [#latest-version] | Package | Version | | -------------------------------- | ------- | | `@petify/react-native-camera-ui` | 1.5.0 | **Server API recommended**: Pet management such as registration, verification, and identification should be performed on the server via the [Server API](/petify/docs/server/api/overview). The client only handles capture through the camera UI. *** ## Package Features [#package-features] `@petify/react-native-camera-ui` is a **UI layer** wrapper around the iOS/Android native camera SDK (`CameraController` + `CameraView`). It provides the camera preview, detection overlays (nose/face markers, progress ring), and attention sounds as-is. * **Hook-based API**: `usePetnowCamera({ apiKey })` returns a controller handle that owns the license and detection commands. * **Bare component**: `` renders the native preview + overlay. * **Automatic single-hardware-session management**: RN views are frequently destroyed and recreated by navigation, re-renders, and hot reloads, but the camera session is managed by the package as a singleton, so the view does not own it. After the last detach, the session is reused for a short grace period to reduce re-initialization cost. However, on Android only the most recently attached view has the preview, so there may be a brief gap where the previous/new view's preview is empty during a transition. ## SDK Structure [#sdk-structure] ### Key Components [#key-components] * `usePetnowCamera({ apiKey })` * Returns a **controller handle** that holds the license and exposes detection commands. * Exposes the SDK readiness state via `camera.state`. * `` * Binds to the controller and renders the native preview + detection overlay. * The camera is initialized on mount and cleaned up on unmount. ## Two-Tier Lifecycle [#two-tier-lifecycle] The camera (view lifetime) is the outer tier, and detection is a sub-lifecycle that runs inside it. * **The camera** follows the view lifetime (mount = `initializeCamera`, unmount = `finalize`). * **Detection** auto-starts on mount, and afterward pause, resume, and re-capture (`startDetection`) are controlled through hook commands (`pauseDetection` preserves progress). ## Technical Requirements [#technical-requirements] * **React Native**: 0.75 or newer, tested through 0.86.x, with the **New Architecture (Fabric/TurboModules) enabled** (0.86 requires React 19 and SDK 1.4.4+) * **Android**: minSdk 28 or higher. The host activity must be a `PermissionAwareActivity` (RN's default `ReactActivity` satisfies this — no separate `FragmentActivity` required) * **iOS**: 16.4 or higher * **Petnow API key** (if you don't have one, contact [support@petnow.io](mailto:support@petnow.io)) ## Next Steps [#next-steps] * [Getting Started](/petify/docs/sdk/react-native/getting-started) — Installation and initial setup * [Basic Usage](/petify/docs/sdk/react-native/basic-usage) — hook + CameraView integration * [Customization](/petify/docs/sdk/react-native/customization) — Layering UI on top of `` # Sound Guide Source: /petify/docs/sdk/react-native/sound > Play attention sounds with PetnowSound to draw a pet's attention. *** Complete [Basic Usage](/petify/docs/sdk/react-native/basic-usage) before starting this guide. ## Overview [#overview] `@petify/react-native-camera-ui` includes six built-in sounds for drawing a pet's attention. Use the `PetnowSound` API to encourage a pet to look at the camera during capture. It works independently of the camera, so it plays even without a camera view. ## Available Sounds [#available-sounds] ### Full sound list [#full-sound-list] **Sound preview** Click each sound to preview it. Test how your pet reacts! | Sound | Description | Target | Preview | | ---------------- | ------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------- | | 🥔 `bagOfChips` | Crinkling chip bag | 🐕🐱 Both | | | 🥫 `openingCan` | Can-opening sound | 🐕🐱 Both | | | 🍚 `pouringFood` | Pouring kibble | 🐕🐱 Both | | | 🥣 `shakers` | Shaking kibble | 🐕🐱 Both | | | 🧸 `squeakyToy` | Squeaky toy sound | 🐕 Dog | | | 😽 `pspsps` | Pspsps sound | 🐱 Cat | | ## Basic Usage [#basic-usage] Load the samples once with `prepare()` before playing any sound. ```tsx import { PetnowSound } from '@petify/react-native-camera-ui'; await PetnowSound.prepare(); // Load samples (first time only) ``` ### 1. Play a random sound [#1-play-a-random-sound] Automatically selects and plays a sound suitable for the species. ```tsx // Random sound for a dog const handle = PetnowSound.play('random', 'DOG'); // Random sound for a cat const handle = PetnowSound.play('random', 'CAT'); ``` ### 2. Play a specific sound [#2-play-a-specific-sound] Choose and play a specific sound directly. ```tsx // Play the squeaky toy sound const handle = PetnowSound.play('squeakyToy', 'DOG'); // Play the pspsps sound const handle = PetnowSound.play('pspsps', 'CAT'); ``` ### 3. Stop a sound [#3-stop-a-sound] To stop a sound, keep the handle returned when you played it. ```tsx // ⚠️ You won't be able to call stop() this way PetnowSound.play('squeakyToy', 'DOG'); // return handle ignored // ✅ Keep the handle to stop it const handle = PetnowSound.play('squeakyToy', 'DOG'); // ... later PetnowSound.stop(handle); ``` If `play()` returns `-1`, the sound did not play (`prepare()` was not called, or the sound is not mapped for the species). In that case, `stop()` does nothing. ### 4. Release resources [#4-release-resources] When you no longer need sounds, release the native audio resources. ```tsx PetnowSound.release(); ``` ### Method summary [#method-summary] | Method | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `prepare()` | Load samples (`Promise`). Call once before `play` — ideally ahead of time (e.g. at mount). iOS is on-demand (resolves immediately); Android preloads and resolves after loading completes | | `play(sound, species)` | Returns a stop handle (`number`, ≥0) after playing. `-1` if not prepared/not mapped | | `stop(handle)` | Stop with the handle | | `release()` | Release native audio resources | * `sound`: `'random'` (selects a candidate suitable for the species) | `'pouringFood'` | `'bagOfChips'` | `'openingCan'` | `'squeakyToy'` | `'pspsps'` | `'shakers'` * `species`: `'DOG'` | `'CAT'` — used for `'random'` candidate selection (ignored for other sounds). ## Next Steps [#next-steps] * [Basic Usage](/petify/docs/sdk/react-native/basic-usage) — `usePetnowCamera` + `CameraView`, events, commands, and lifecycle * [Customization](/petify/docs/sdk/react-native/customization) — Layering guide, buttons, and result UI on top of `` # 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