Petnow LogoPetnow

Server Integration

The Server Integration API for issuing capture sessions from your server and receiving trusted results.

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.

ItemValue
Base URLhttps://api.petify.petnow.io
Authenticationx-petnow-api-key header (issue an API Key)
Response formatSuccess: { "success": true, "data": ... } / Error: { "errors": [{ "code": "PETNOWB2B..." }] }

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

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 (/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

  1. Issue an API Key in the 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 for the registration rules.

Issuing a session

Endpoint: POST /v2/hosted-sessions

Request

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"
  }'
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
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

FieldTypeRequiredDescription
actionstringREGISTER / VERIFY / IDENTIFY
speciesstringDOG or CAT. For VERIFY it must match the target pet's species; IDENTIFY searches for candidates only among pets registered under this species
redirectUrlstringThe URL to return to on completion. Must exactly match an allow-list entry
externalPetIdstringConditionalYour system's pet ID (up to 255 characters). Required for REGISTER/VERIFY; must not be sent for IDENTIFY
petMetadatastringREGISTER only. A free-form string stored on the registered pet's metadata field
expiresInSecondsintegerSession expiry (300–86400 seconds, default 1800)
localestringLanguage hint for the capture page (ko default, en supported)

Response (201)

{
  "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"
  }
}
  • 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. Treat it as single-use — avoid designs that re-send the same URL over multiple channels.

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 for the full parameter rules.

Consuming the result code

Trade the one-time result code received with petify_status=completed on your server.

Endpoint: GET /v2/hosted-results/{code}

curl -X GET "https://api.petify.petnow.io/v2/hosted-results/hrc_1a2b3c4d5e6f" \
  -H "x-petnow-api-key: YOUR_API_KEY"
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"]
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):

{
  "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:

ActionFieldsDescription
REGISTERregistered (boolean)Whether the registration succeeded
VERIFYisVerified (boolean), score (0–100)Match result and score
IDENTIFYcandidates (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

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}

{
  "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):

StateDescription
CREATEDThe session was issued and not yet opened
OPENEDThe user opened the capture page
PROCESSINGSubmitted and being processed
COMPLETEDFinished normally — outcome and result are populated
FAILEDFailed — errorCode carries the reason (e.g. CAPTURE_FAILED)
EXPIREDExpired without completing
CANCELLEDThe user cancelled the capture
  • outcome is populated only for COMPLETED: SUCCEEDED for Register, MATCH / NO_MATCH for Verify and Identify.
  • This endpoint also serves sessions created from Link Integration links (link sessions carry a linkId, and a user-supplied externalPetId is marked bindingTrusted: false).

Error codes

Error responses use the { "errors": [{ "code": "..." }] } shape. PETNOWB2B10001 (request validation failure) additionally carries per-field details in details.

CodeHTTPMeaning
PETNOWB2B10000401Missing or invalid API Key
PETNOWB2B10001400Request validation failed (details carries per-field reasons)
PETNOWB2B10005402Registered-pet limit reached
PETNOWB2B10010403Account suspended for billing reasons
PETNOWB2B10011402Identification (IDENTIFY) is not included in the current plan
PETNOWB2B10012402A plan must be selected (choose a plan in the Console first)
PETNOWB2B20015400species does not match the VERIFY target pet
PETNOWB2B21001409REGISTER: the externalPetId is already registered
PETNOWB2B21002404VERIFY: the externalPetId has no registration
PETNOWB2B21003400The redirectUrl is not on the allow-list
PETNOWB2B21004404Session not found
PETNOWB2B21005404Result code unknown or expired (Link Integration display codes are also a 404 on this route)
PETNOWB2B21006410Result code already consumed (grace window elapsed)
PETNOWB2B21007503Session issuance temporarily disabled — retry with backoff, not in a tight loop

Next steps

On this page