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.
| Item | Value |
|---|---|
| Base URL | https://api.petify.petnow.io |
| Authentication | x-petnow-api-key header (issue an API Key) |
| Response format | Success: { "success": true, "data": ... } / Error: { "errors": [{ "code": "PETNOWB2B..." }] } |
The full round-trip
- Your server issues a session via
POST /v2/hosted-sessionsand delivers the one-time web URL from the response to the end user. - When the user finishes capturing on the Petnow-hosted page, the browser returns to your
redirectUrl. On completion,petify_codecarries a one-time result code. - Your server trades the code for the trusted result via
GET /v2/hosted-results/{code}(one-shot consume). - 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
- Issue an API Key in the Petify Console. (It can also be managed from the Console's Integration → API & SDK tab.)
- 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 userconst 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 userRequest 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) |
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
idtogether with your own user/order at issuance time — matching it against the result'ssessionId/externalPetIdtells you whose result it is, and it is also your only way to read the result if the redirect is lost. - The
urlis 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:
| 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
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):
| 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 (e.g. CAPTURE_FAILED) |
EXPIRED | Expired without completing |
CANCELLED | The user cancelled the capture |
outcomeis populated only forCOMPLETED:SUCCEEDEDfor Register,MATCH/NO_MATCHfor Verify and Identify.- This endpoint also serves sessions created from Link Integration links (link sessions carry a
linkId, and a user-suppliedexternalPetIdis markedbindingTrusted: false).
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) |
PETNOWB2B21007 | 503 | Session issuance temporarily disabled — retry with backoff, not in a tight loop |
Next steps
- Redirect URLs & Result Delivery - Allow-list management and redirect parameters
- Link Integration - Issue and manage links in the Console