API methods
Partner API documentation
This reference describes how your integration talks to Animal-ID on behalf of your organization: authentication, required headers and the core MVP endpoints (owners, animals, procedures, photos, dictionaries).
SDKs & client libraries
Official open-source SDKs that wrap request signing and the endpoints below — drop them in instead of signing requests by hand.
PHP · Composer
Server-side PHP SDK: HMAC request signing and typed clients for every endpoint.
composer require animal-id/aid-partner-sdk
JavaScript · npm
Framework packages for Node and the browser, built on a shared core.
npm i @animal-id/partner-core
- @animal-id/partner-coreFramework-agnostic core: signing + typed client.
- @animal-id/partner-reactReact hooks & helpers.
- @animal-id/partner-vueVue composables.
- @animal-id/partner-angularAngular services.
- @animal-id/partner-nestjsNestJS module for server-side integrations.
Authentication & request signing
Base URL: https://gw.animal-id.net · all paths are prefixed with /v1/partner.
Every signed request carries four headers. The signature is an HMAC-SHA256 (hex) of a canonical string, keyed with your private key:
stringToSign = METHOD + "\n" + path[?query] + "\n" + sha256_hex(rawBody) + "\n" + timestamp signature = hex( hmac_sha256(stringToSign, privateKey) )
| Header | Value |
|---|---|
X-Eternity-App-Id | Your application id. |
X-Eternity-Public-Key | Your public key. |
X-Eternity-Timestamp | Unix seconds; must be within ±300s of server time. |
X-Eternity-Signature | The HMAC-SHA256 hex computed above. |
X-Eternity-Idempotency-Key | UUID, required on every POST/PATCH/DELETE. Replays return the first response; same key + different body → 409. |
X-Eternity-Animal-ID-Version | Optional date version (YYYY-MM-DD). Defaults to the version locked when your app was issued; from 2026-07-04, animal registration attaches existing owners by public_id instead of user_gid. |
X-Eternity-Expand | Optional. JSON array of expand keys to embed extra data on supported endpoints (e.g. ["owners"]). See an endpoint's “Expansion” section for its allowed keys; unknown keys → 422. |
X-Eternity-Lang-Code | Optional. Language code (uk, en, ru, de, es) for the names the API returns — e.g. species_name and breed_name on an animal card, and the dictionary labels. Defaults to the region default when omitted. |
Sign and send the exact same body bytes. For GET/DELETE the body is empty (its sha256 is the hash of an empty string). path includes the query string when present. For multipart/form-data uploads (photos) the raw body is not part of the signature either — sign with the sha256 of an empty body.
Every successful response wraps results in a payload array. Single-resource create/get endpoints return a one-element array (e.g. { "payload": [ { … } ] }); the per-endpoint examples below show the single object for brevity.
APP_ID="aid_app_xxx"; PUBLIC_KEY="pk_xxx"; PRIVATE_KEY="sk_xxx"
METHOD="POST"
PATH_Q="/v1/partner/owners" # path (+ "?query" if any), exactly as sent
BODY='{"email":"jane@example.com","consent":{"account_creation":true}}'
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 | awk '{print $2}')
STRING_TO_SIGN=$(printf '%s\n%s\n%s\n%s' "$METHOD" "$PATH_Q" "$BODY_HASH" "$TS")
SIG=$(printf '%s' "$STRING_TO_SIGN" | openssl dgst -sha256 -hmac "$PRIVATE_KEY" | awk '{print $2}')
curl -X "$METHOD" "https://gw.animal-id.net$PATH_Q" \
-H "X-Eternity-App-Id: $APP_ID" \
-H "X-Eternity-Public-Key: $PUBLIC_KEY" \
-H "X-Eternity-Timestamp: $TS" \
-H "X-Eternity-Signature: $SIG" \
-H "X-Eternity-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d "$BODY"const crypto = require('crypto');
function sign({ method, pathQ, body = '', privateKey }) {
const ts = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const stringToSign = [method, pathQ, bodyHash, ts].join('\n');
const signature = crypto.createHmac('sha256', privateKey).update(stringToSign).digest('hex');
return { ts, signature };
}
const body = JSON.stringify({ email: 'jane@example.com' });
const { ts, signature } = sign({ method: 'POST', pathQ: '/v1/partner/owners', body, privateKey: 'sk_xxx' });
await fetch('https://gw.animal-id.net/v1/partner/owners', {
method: 'POST',
headers: {
'X-Eternity-App-Id': 'aid_app_xxx',
'X-Eternity-Public-Key': 'pk_xxx',
'X-Eternity-Timestamp': ts,
'X-Eternity-Signature': signature,
'X-Eternity-Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json',
},
body, // sign and send the SAME bytes
});import hashlib, hmac, time, json, uuid, requests
def sign(method, path_q, body, private_key):
ts = str(int(time.time()))
body_hash = hashlib.sha256(body.encode()).hexdigest()
string_to_sign = "\n".join([method, path_q, body_hash, ts])
signature = hmac.new(private_key.encode(), string_to_sign.encode(), hashlib.sha256).hexdigest()
return ts, signature
body = json.dumps({"email": "jane@example.com"}, separators=(",", ":"))
ts, signature = sign("POST", "/v1/partner/owners", body, "sk_xxx")
requests.post(
"https://gw.animal-id.net/v1/partner/owners",
data=body, # send the SAME bytes you signed
headers={
"X-Eternity-App-Id": "aid_app_xxx",
"X-Eternity-Public-Key": "pk_xxx",
"X-Eternity-Timestamp": ts,
"X-Eternity-Signature": signature,
"X-Eternity-Idempotency-Key": str(uuid.uuid4()),
"Content-Type": "application/json",
},
)<?php
function signRequest(string $method, string $pathQ, string $body, string $privateKey): array {
$ts = (string) time();
$bodyHash = hash('sha256', $body);
$stringToSign = implode("\n", [$method, $pathQ, $bodyHash, $ts]);
$signature = hash_hmac('sha256', $stringToSign, $privateKey);
return [$ts, $signature];
}
$body = json_encode(['email' => 'jane@example.com']);
[$ts, $signature] = signRequest('POST', '/v1/partner/owners', $body, 'sk_xxx');
$ch = curl_init('https://gw.animal-id.net/v1/partner/owners');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body, // send the SAME bytes you signed
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"X-Eternity-App-Id: aid_app_xxx",
"X-Eternity-Public-Key: pk_xxx",
"X-Eternity-Timestamp: $ts",
"X-Eternity-Signature: $signature",
'X-Eternity-Idempotency-Key: ' . bin2hex(random_bytes(16)),
'Content-Type: application/json',
],
]);
$response = curl_exec($ch);Webhooks
Receive deferred events — like an owner approving a vet's access request — as signed POST requests to your server. Set the URL and find your signing secret on your API keys tab.
Signed webhook deliveryWebhook signature & payload▸We POST every event to your configured webhook URL, signed with your per-app webhook secret (shown once when generated). Verify the signature before trusting a delivery by recomputing the HMAC over the canonical string and comparing in constant time:
canonical = "POST" + "\n" + path[?query] + "\n" + sha256_hex(rawBody) + "\n" + timestamp signature = hex( hmac_sha256(canonical, webhookSecret) )
| Header | Value |
|---|---|
X-Eternity-Webhook-Id | Unique delivery id (UUID); stable across resends of the same event. |
X-Eternity-Webhook-Event | The event key, e.g. animal_access.approved. |
X-Eternity-Webhook-Timestamp | Unix seconds when the delivery was signed; reject deliveries with a skewed timestamp. |
X-Eternity-Webhook-Signature | HMAC-SHA256 hex of the canonical string, keyed with your webhook secret (not your API private key). |
Delivery body (JSON)
{
"id": "5f1c0b8e-3a2d-4c7b-9b1a-2e6f0d4c8a91",
"event": "animal_access.approved",
"occurred_at": "2026-06-24T09:15:00+00:00",
"result": {
"animal_id": "8xK3pQzVnB7rL2qF",
"requester_user_gid": 90231,
"status": "granted",
"requested_at": "2026-06-23T09:00:00+00:00",
"expires_at": "2026-06-30T09:00:00+00:00",
"retry_after_seconds": 0,
"decided_at": "2026-06-24T09:15:00+00:00"
}
}| Field | Description |
|---|---|
id | The unique event id (matches the X-Eternity-Webhook-Id header). |
event | The event key, e.g. animal_access.approved. |
occurred_at | When the event occurred (ISO 8601). |
result | The access decision. Mirrors GET /v1/partner/animals/{id}/access-request and adds animal_id, requester_user_gid and decided_at. |
Events
| Key | Meaning |
|---|---|
animal_access.approved | An owner approved your pending access request — you may now edit the animal. |
animal_access.denied | An owner denied your access request — you can request again after it expires. |
procedures.verified | An owner confirmed the procedures you recorded without access — they are verified now. |
procedures.rejected | An owner declined the procedures you recorded without access — they were removed. |
consent.approved | The person you asked allowed it — you may act on the permission until it expires. |
consent.denied | The person you asked declined. Nothing was granted. |
consent.revoked | A permission you were given was taken back. For a handed-over key it stops working immediately. |
consent.expired | Nobody answered in time and the request lapsed. This is not a refusal — you may ask again. |
Procedures event payload
Sent to the organization the access request was made for, in addition to animal_access.* and in no fixed order with it. The changes a verified procedure makes to the animal (chip, tag, sterilization, death) are applied separately and may land after this event.
{
"id": "0c9b6a14-6d1e-4b8e-9f7a-3c2d1e0f9a87",
"event": "procedures.verified",
"occurred_at": "2026-05-31T10:00:00+00:00",
"result": {
"animal_id": "8xK3pQzVnB7rL2qF",
"requester_user_gid": 90231,
"decided_at": "2026-05-31T10:00:00+00:00",
"procedures": [
{ "id": 99001, "visit_id": null, "type": 10,
"occurred_at": "2026-05-30T08:00:00+00:00", "verification_status": "verified" }
]
}
}| Field | Description |
|---|---|
animal_id | Animal public id. |
requester_user_gid | Global id of the vet who recorded the procedures. |
decided_at | When the owner decided (ISO 8601). |
procedures[] | Each procedure the decision covered: id, visit_id (null for an unverified batch), type, occurred_at and its new verification_status. |
Consent event payload
Consent events are delivered to your platform application rather than to an organization: a doctor allowing you to hold credentials that act as them involves no clinic at all.
{
"id": "282824f1-b65e-40f4-86d1-62a2bac566df",
"event": "consent.revoked",
"occurred_at": "2026-08-26T19:45:00+00:00",
"result": {
"consent_id": "qeHSuvlSZHAWpacF",
"kind": "key_handover",
"status": "revoked",
"doctor_id": "c71ORqKq81fRoTcm",
"clinic_id": null,
"decided_at": "2026-08-26T19:45:00+00:00"
}
}| Field | Description |
|---|---|
consent_id | The request this outcome belongs to — the same id the provisioning call returned. |
kind | key_handover (credentials that act as a doctor) or clinic_membership (seating a doctor in a clinic). |
status | approved, denied, revoked or expired. |
doctor_id | The doctor's public id, for a key handover. Null otherwise, and null if the account has since been removed. |
clinic_id | The clinic's public id, for a clinic membership. Null otherwise. |
decided_at | When it reached this state (ISO 8601). For an expiry this is the deadline, not when we noticed. |
Handle consent.revoked. A permission can be withdrawn at any time, and a handed-over key stops working the moment it is — without this event you would only find out when your next call fails.
Acknowledge with any 2xx response. A non-2xx status or a timeout is recorded as a failed delivery, which you can resend from the webhook delivery log in your cabinet.
const crypto = require('crypto');
// Express: mount with a raw-body parser so you verify the EXACT bytes received.
// app.post('/animal-id/webhook', express.raw({ type: 'application/json' }), handler)
function verify(req, webhookSecret) {
const ts = req.header('X-Eternity-Webhook-Timestamp');
const sig = req.header('X-Eternity-Webhook-Signature');
const path = req.originalUrl; // path (+ ?query) exactly as received
const raw = req.body; // Buffer of the raw request body
const bodyHash = crypto.createHash('sha256').update(raw).digest('hex');
const canonical = ['POST', path, bodyHash, ts].join('\n');
const expected = crypto.createHmac('sha256', webhookSecret).update(canonical).digest('hex');
return sig && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}<?php
// Verify a webhook delivery from Animal ID before trusting it.
function verifyWebhook(string $rawBody, string $path, string $webhookSecret): bool {
$ts = $_SERVER['HTTP_X_ETERNITY_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_ETERNITY_WEBHOOK_SIGNATURE'] ?? '';
$canonical = implode("\n", ['POST', $path, hash('sha256', $rawBody), $ts]);
$expected = hash_hmac('sha256', $canonical, $webhookSecret);
return $sig !== '' && hash_equals($expected, $sig);
}
$rawBody = file_get_contents('php://input'); // verify the EXACT bytes received
if (!verifyWebhook($rawBody, $_SERVER['REQUEST_URI'], getenv('AID_WEBHOOK_SECRET'))) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody, true); // ['id' => …, 'event' => …, 'result' => …]
http_response_code(204); // acknowledge with any 2xxAPI methods
Start typing — sections filter by keyword.
Platform API — provisioning
Setting up clinics and doctors, and asking people for permission you cannot take. This is where an integration begins: you create the accounts, then collect the credentials they will be worked with.
- Signed with your platform key, issued to you once when your partner account is set up. Send the platform application uuid in X-Eternity-App-Id.
- It never reaches animal data. A platform key on a /v1/partner/ path answers 401 — that is the separation working, not a misconfiguration.
/v1/platform/organizationsFind a clinic that already exists before creating one.▸Auth: HMAC with your PLATFORM key.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
query | yes | string | — | Name or address fragment, at least 2 characters. |
limit | no | int | — | Maximum results, up to 50 (default 20). |
Example response
{
"payload": [
{
"public_id": "TGo1Gwe2ppRkUFO1",
"org_name": "Лапа",
"full_address": "Україна, Київ, вул. Прикладна, 1",
"status": 3,
"linked": false
}
]
}| Field | Description |
|---|---|
public_id | Pass it to the member endpoint to seat a doctor here. |
status | Moderation status. A clinic you provisioned stays out of the public directory until its director claims it. |
linked | true when this clinic is one you provisioned yourself. |
Response statuses
| Status | Meaning |
|---|---|
200 | Matches (an empty list when nothing matches). |
422 | Validation error (query missing or shorter than 2 characters). |
/v1/platform/organizationsCreate or resolve a clinic by your own identifier.▸Auth: HMAC with your PLATFORM key. Issued separately from clinic keys; it never reaches animal data.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
external_org_id | yes | string | — | Your own identifier for this clinic. A repeat call with the same value returns the clinic you already have instead of creating another, so send a stable value. |
name | yes | string | — | Clinic name. |
director_public_id | yes | string | — | The doctor who takes the director seat, by their public_id (from POST /owners or the member endpoint below). A clinic cannot exist without a director, and a doctor may direct only one clinic — naming someone who already directs elsewhere is answered with 409. |
email | no | string | — | Contact email. |
phone | no | string | — | Contact phone (E.164). |
website | no | string | — | Website. |
address | no | string | — | Full address. |
country_id | no | int | countries | Country dictionary id. |
description | no | string | — | Free-text description. |
lat | no | number | — | Latitude. |
lng | no | number | — | Longitude. |
Request body (JSON)
{
"external_org_id": "crm-clinic-118",
"name": "Лапа",
"director_public_id": "V1StGXR8Z5jd",
"email": "clinic@example.com",
"phone": "+380441234567"
}Example response
{
"payload": {
"public_id": "yAvgJrSYehJo9JXh",
"name": "Лапа",
"status": 2,
"created": true
}
}| Field | Description |
|---|---|
public_id | Stable public clinic identifier — pass it into the member endpoint below. |
created | true when this call created the clinic; false when it resolved the one you already had. |
status | Moderation status. A provisioned clinic stays out of the public catalogue until its director claims it. |
Response statuses
| Status | Meaning |
|---|---|
201 | Clinic created. |
200 | Clinic already existed for this external_org_id — same clinic returned. |
409 | The named director already directs another clinic. Name a different doctor. |
422 | Validation error (missing external_org_id, name or director_public_id). |
/v1/platform/organizations/{clinic_public_id}/membersSeat a doctor in a clinic and receive their credentials.▸Auth: HMAC with your PLATFORM key.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
email | conditional | string | — | Doctor's email. One of email/phone is required. |
phone | conditional | string | — | Doctor's phone (E.164). One of email/phone is required. |
first_name | no | string | — | Given name. |
last_name | no | string | — | Family name. |
language | no | string | languages | Preferred locale. |
clinic_name | no | string | — | Label shown on the issued credentials. |
external_doctor_id | no | string | — | Your own identifier for this doctor. Same field and same rules as external_owner_id: written once, never overwritten, and only ever visible to you. |
consent | yes | object | — | Consent block (immutable audit). |
consent.account_creation | yes | bool | — | Must be true. The credentials you receive act as this person, so their agreement is required. |
Request body (JSON)
{
"email": "doctor@example.com",
"first_name": "Ihor",
"last_name": "Melnyk",
"language": "uk",
"external_doctor_id": "crm-doc-4471",
"consent": { "account_creation": true }
}Example response
{
"payload": {
"public_id": "LSG9w6B2rwAiPoJD",
"app_id": "4833d2e5-1a97-4572-ac5d-a03c7614e0d7",
"public_key": "94af0494...",
"private_key": "b208c6e0...",
"created": true
}
}| Field | Description |
|---|---|
app_id | App-Id this doctor signs data-plane calls with (X-Eternity-App-Id). |
public_key | Public half of the doctor's key pair. |
private_key | Private half. Shown in this response and never again — store it before you close the connection. |
public_id | The doctor's stable public identifier — use it wherever an owner or director is named. |
created | true when the account was created by this call; false when the doctor already had one and was matched. |
Response statuses
| Status | Meaning |
|---|---|
201 | Doctor seated; credentials returned. |
409 | This doctor already holds a key for this clinic, or the clinic is not one you provisioned and its director has not approved you — raise a clinic_membership consent first. |
422 | Validation error (no email/phone, or consent.account_creation not accepted). |
/v1/platform/organizations/{clinic_public_id}/members/{doctor_public_id}/credentialsTake credentials acting as a doctor who agreed to it.▸Auth: HMAC with your PLATFORM key.
Example response
{
"payload": {
"public_id": "LSG9w6B2rwAiPoJD",
"app_id": "dcd4895e-550e-424a-9c5d-5206cbce2956",
"public_key": "11fff5d1...",
"private_key": "00604b9b...",
"created": false
}
}| Field | Description |
|---|---|
private_key | Shown once and never again. The key is held by you and is separate from the doctor's own — when they withdraw consent yours stops working and theirs does not. |
Response statuses
| Status | Meaning |
|---|---|
201 | Credentials issued. |
409 | The doctor has not agreed to the handover, or the clinic is not one you may act on. The message names which. |
/v1/platform/consentsAsk a doctor or a clinic for permission.▸Auth: HMAC with your PLATFORM key.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
kind | yes | string | — | key_handover — hold credentials that act as this doctor, which only the doctor can allow. clinic_membership — seat this doctor in a clinic you did not provision, which its director decides. |
doctor_public_id | yes | string | — | The doctor the request concerns. |
clinic_public_id | conditional | string | — | Required for clinic_membership: the clinic whose director you are asking. |
Request body (JSON)
{
"kind": "clinic_membership",
"doctor_public_id": "LSG9w6B2rwAiPoJD",
"clinic_public_id": "jJlmHf0sgxMT3Gni"
}Example response
{
"payload": {
"public_id": "5G5rBYl0hQvpJuCZ",
"kind": "clinic_membership",
"status": "pending",
"expires_at": 1788973565,
"decided_at": null
}
}| Field | Description |
|---|---|
public_id | Poll the status endpoint with this. |
status | pending until the person answers, then approved or denied. |
expires_at | Unix seconds. An unanswered request closes itself, and an approval stops working at the same moment — permission is not permanent. |
Response statuses
| Status | Meaning |
|---|---|
201 | Request raised, or the one you already had returned — asking twice does not create two. |
409 | Nobody can decide this (for example, a clinic with no director). |
422 | Validation error (unknown kind, or clinic_public_id missing for clinic_membership). |
/v1/platform/consents/{public_id}Check where a permission request stands.▸Auth: HMAC with your PLATFORM key.
Example response
{
"payload": {
"public_id": "5G5rBYl0hQvpJuCZ",
"kind": "clinic_membership",
"status": "approved",
"expires_at": 1788973565,
"decided_at": 1787763211
}
}Response statuses
| Status | Meaning |
|---|---|
200 | Current state. |
404 | No such request, or it belongs to another partner. |
Partner API — data
Animals, owners, procedures and photos — the day-to-day surface. Every write here is recorded as the doctor whose key signed it.
- Signed with a doctor's key, returned once when you seat them in a clinic or collect their credentials after they agree. Send that doctor's application uuid in X-Eternity-App-Id.
- It cannot provision anything, and it stops working the moment the doctor withdraws their permission. Dictionaries need no signature at all.
/v1/partner/dictionariesReference dictionaries (multilang, filterable).▸Auth: Public (no signature required). CDN-cacheable via ETag.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
include | no | string (csv) | — | Comma-separated dictionary keys to return; empty → all. Keys: species, pet_species_featured, sex, sizes, lost_statuses, other_identifiers, procedure_types, countries, languages, cites. `pet_species_featured` is the canonical popular-species list keyed by a UUID public_id — pass its code into POST /animals as species_public_id. |
q | no | string | — | Filter entries by localized name in any active language. |
lang | no | string | languages | Project names to a single locale (uk, en, ru, de, es). Default: all active. |
Example response
{
"payload": [
{
"key": "species",
"items": [
{ "code": 3, "names": { "uk": "Собаки", "en": "Dogs", "ru": "Собаки", "de": "Dogs" } },
{ "code": 4, "names": { "uk": "Коти", "en": "Cats" } }
]
},
{
"key": "pet_species_featured",
"items": [
{ "code": "1b289672-0a74-48d2-9600-7104bedacd8c", "names": { "uk": "Собака", "en": "Dog" } },
{ "code": "b0dd3c3a-4d35-4479-91f9-616ae072c400", "names": { "uk": "Домашні коти", "en": "Domestic cats" } }
]
},
{
"key": "countries",
"items": [
{ "code": "804", "alpha2": "UA", "alpha3": "UKR",
"names": { "uk": "Україна", "en": "Ukraine", "ru": "Украина", "de": "Ukraine", "es": "Ukraine" } }
]
},
{
"key": "languages",
"items": [
{ "code": "uk", "native": "Українська",
"names": { "uk": "Українська", "en": "Ukrainian", "ru": "Украинский", "de": "Ukrainian", "es": "Ukrainian" } }
]
}
],
"metadata": { "etag": "W/\"dict-…\"", "generated_at": "2026-05-30T08:00:00+00:00", "languages": ["uk","en","ru","de","es"] },
"links": [],
"message": null
}| Field | Description |
|---|---|
payload[].key | Dictionary key. |
payload[].items[].code | Stable id used as the value in write endpoints. Numeric for most dictionaries (species, sex, …); for pet_species_featured it is the canonical species public_id (a UUID) → pass it as species_public_id on POST /animals; for countries it is the zero-padded ISO 3166-1 numeric code as a string (e.g. "004", "804"); for languages it is the ISO 639-1 code (e.g. "uk"). |
payload[].items[].names | Map locale → localized name. Locales without a translation fall back to English. |
payload[].items[].alpha2 / alpha3 | Countries only: ISO 3166-1 alpha-2 / alpha-3 codes for convenient mapping. |
payload[].items[].native | Languages only: the language's own name (endonym), handy for language pickers. |
metadata.etag | Pass back in If-None-Match to get 304 when unchanged. |
metadata.languages | Active locales actually present in this build. |
Response statuses
| Status | Meaning |
|---|---|
200 | OK — dictionaries returned. |
304 | Not Modified — your If-None-Match matches; reuse the cached copy. |
/v1/partner/dictionaries/speciesSpecies search (canonical, by public_id).▸Auth: Public (no signature required).
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
q | no | string | — | Species search term (any taxonomic rank, substring + typo-tolerant). Omit to browse the top matches. Complements the popular-only pet_species_featured dictionary. |
Example response
{
"payload": [
{ "public_id": "1b289672-0a74-48d2-9600-7104bedacd8c", "name": "Dog", "rank": "species", "has_breeds": true },
{ "public_id": "9cd7d2a1-4e45-4922-8f9e-fb84c61ca6ae", "name": "Cats", "rank": "genus", "has_breeds": false }
]
}| Field | Description |
|---|---|
payload[].public_id | Species public_id — pass it as species_public_id on POST /animals, or into /dictionaries/species/{species_public_id}/breeds. |
payload[].name | Localized species name (request locale, English fallback). |
payload[].rank | Taxonomic rank (species, genus, family, …). |
payload[].has_breeds | Whether the species carries a breed list. |
Response statuses
| Status | Meaning |
|---|---|
200 | OK — matching species. |
/v1/partner/dictionaries/species/{species_public_id}/breedsBreeds for a species (canonical, by public_id).▸Auth: Public (no signature required).
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
species_public_id | yes | string | — | Path segment: the species public_id (a UUID). Discover it via pet_species_featured or /dictionaries/species. |
q | no | string | — | Optional breed-name filter (substring, typo-tolerant). |
Example response
{
"payload": [
{ "public_id": "6bb37956-d7fb-426d-8045-9d2fb854ee43", "name": "Belgian Shepherd Dog", "kind": "purebred" },
{ "public_id": "d1f2a0c8-7b3e-4a1d-9c22-0f5e6b7a8c90", "name": "Mixed breed", "kind": "mixed" }
]
}| Field | Description |
|---|---|
payload[].public_id | Canonical breed public_id — pass it as breed_public_id on POST /animals. |
payload[].name | Localized breed name (request locale, English fallback). |
payload[].kind | purebred | mixed | unknown | crossbreed | variety. mixed/unknown/crossbreed are the special sentinel options. |
Response statuses
| Status | Meaning |
|---|---|
200 | OK — breeds for the species (an empty list for a species with no breeds). |
404 | species_public_id is not a valid UUID (the route did not match). |
/v1/partner/ownersCreate (or resolve) an owner; returns the global user id.▸Auth: HMAC. Any partner key. · X-Eternity-Idempotency-Key required
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
email | conditional | string | — | Owner email. One of email/phone is required — it is how the owner is later reached/authenticated. |
phone | conditional | string | — | Owner phone (E.164). One of email/phone is required. |
first_name | no | string | — | Given name. |
last_name | no | string | — | Family name. |
language | no | string | languages | Preferred locale. |
country | no | string | countries | Zero-padded ISO 3166-1 numeric code as a string (e.g. "804") — matches the countries dictionary code. |
external_owner_id | no | string | — | Your own identifier for this person. Stored once, on first contact, and never overwritten — send it so both sides can reconcile the same human later. Max 128 characters. |
consent | yes | object | — | Consent block (immutable audit). |
consent.account_creation | yes | bool | — | Must be true — the owner agreed to account creation. The time of capture is recorded server-side. |
Request body (JSON)
{
"email": "jane@example.com",
"phone": "+380681234567",
"first_name": "Jane",
"last_name": "Doe",
"language": "uk",
"country": "804",
"external_owner_id": "crm-4471",
"consent": {
"account_creation": true
}
}Example response
{
"payload": {
"user_gid": 90231,
"public_id": "V1StGXR8Z5jd",
"has_account": true,
"email": "jane@example.com",
"phone": null,
"display_hint": "Ол*** К.",
"language": "uk",
"country_id": 804,
"external_owner_id": "crm-4471"
}
}| Field | Description |
|---|---|
public_id | Stable public owner id — pass it into POST /animals owners[].public_id (API version >= 2026-07-04). |
user_gid | Legacy numeric owner id (owner-attach on older API versions). |
has_account | Whether the owner already has a usable account. |
email | Email on file for this owner (null if unknown). |
phone | Phone on file for this owner (null if unknown). |
display_hint | Masked display name (no PII). |
external_owner_id | The id YOU sent us for this person, or null if you never did. Scoped to your own integration — you never see what another partner calls the same person. |
Response statuses
| Status | Meaning |
|---|---|
201 | Created (or resolved an existing owner — idempotent by email/phone). |
409 | X-Eternity-Idempotency-Key reused with a different body, or still being processed. |
422 | Validation error (missing email/phone, or consent.account_creation not accepted). |
/v1/partner/owners/searchFind an owner by exact email or phone.▸Auth: HMAC. Any partner key.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
email_or_phone | yes | string | — | Exact email or phone (single field; email detected by format). |
Example response
{
"payload": { "user_gid": 90231, "public_id": "V1StGXR8Z5jd", "has_account": true, "email": "jane@example.com", "phone": null, "display_hint": "Ол*** К.", "language": "uk", "country_id": 804, "external_owner_id": "crm-4471" }
}| Field | Description |
|---|---|
public_id | Stable public owner id — pass it into POST /animals owners[].public_id (API version >= 2026-07-04). |
user_gid | Legacy numeric owner id (owner-attach on older API versions). |
email | Email on file for this owner (null if unknown). |
phone | Phone on file for this owner (null if unknown). |
external_owner_id | The id YOU sent us for this person, or null if you never did. Scoped to your own integration — you never see what another partner calls the same person. |
Response statuses
| Status | Meaning |
|---|---|
200 | Owner found. |
404 | No owner with that email_or_phone. |
422 | email_or_phone is required. |
/v1/partner/animalsRegister an animal (chip-based).▸Auth: HMAC. Vet/organization key. · X-Eternity-Idempotency-Key required
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
species | conditional | int | species | Legacy species dictionary id. Exactly one of species / species_public_id is required. |
species_public_id | conditional | string | pet_species_featured | Canonical species public_id (a UUID) from the pet_species_featured dictionary. Exactly one of species / species_public_id is required; when present it wins over species. |
is_microchip | yes | bool | — | Whether the animal is chipped. true → microchip is required; false → microchip is ignored and the registry assigns a temporary WC number. |
microchip | conditional | string | — | Microchip (transponder). Required only when is_microchip = true. |
nickname | yes | string | — | Animal name. |
qr_tag | no | string | — | QR passport serial to attach at registration. |
owners | no | array | — | Owners; first becomes main_owner, the rest owners (de-duplicated). Each entry either attaches an existing owner OR registers a new one inline. |
owners[].public_id | conditional | string | — | Attach mode (API version >= 2026-07-04): existing owner public_id from POST/GET owners. Omit to register inline. |
owners[].user_gid | conditional | int | — | Attach mode (API versions before 2026-07-04): legacy numeric owner id. Omit to register inline. |
owners[].email | conditional | string | — | Inline mode: owner email. One of email/phone required when there is no public_id/user_gid (upserted — no duplicates). |
owners[].phone | conditional | string | — | Inline mode: owner phone (E.164). |
owners[].first_name | no | string | — | Inline mode: given name. |
owners[].last_name | no | string | — | Inline mode: family name. |
owners[].language | no | string | languages | Inline mode: preferred locale. |
owners[].country | no | string | countries | Inline mode: zero-padded ISO 3166-1 numeric code as a string (e.g. "804") — matches the countries dictionary code. |
owners[].external_owner_id | no | string | — | Inline mode: your own identifier for this person — same field and same rules as on POST /owners. |
owners[].consent.account_creation | conditional | bool | — | Inline mode: must be true — the owner agreed to account creation. Required only when registering inline. |
breed | no | string | — | Breed (free text). Used only when breed_public_id is absent. |
breed_public_id | no | string | — | Canonical breed public_id (a UUID) from GET /breeds?species_public_id=. Must belong to the species; when present it wins over the free-text breed. |
color | no | string | — | Color (free text — no dictionary). |
gender_id | no | int | sex | Gender dictionary id. |
dob | no | date | — | Date of birth (ISO 8601). |
microchip_date | no | date | — | When the chip was implanted (ISO 8601). |
sterilization | no | bool | — | Sterilized flag. |
size | no | int | sizes | Size dictionary id. |
identifiers | no | array | — | Extra identifiers besides microchip/qr. |
identifiers[].type | yes | int | other_identifiers | Identifier type id from the other_identifiers dictionary (tattoo, ring, …). |
identifiers[].value | yes | string | — | Identifier value. |
identifiers[].added_at | no | date | — | When the identifier was assigned (ISO 8601). |
Request body (JSON)
{
"species": 3,
"is_microchip": true,
"microchip": "900263000123456",
"nickname": "Барсік",
"qr_tag": null,
"gender_id": 1,
"breed": "Labrador",
"color": "black",
"dob": "2022-03-01T00:00:00+00:00",
"microchip_date": "2022-11-20T00:00:00+00:00",
"sterilization": true,
"size": 2,
"owners": [
{ "public_id": "V1StGXR8Z5jd" },
{
"email": "jane@example.com",
"phone": "+380681234567",
"first_name": "Jane",
"last_name": "Doe",
"external_owner_id": "crm-4471",
"consent": { "account_creation": true }
}
],
"identifiers": [
{ "type": 3, "value": "TAT-001", "added_at": "2026-05-01T00:00:00+00:00" }
]
}Example response
{ "payload": { "id": "8xK3pQzVnB7rL2qF" } }| Field | Description |
|---|---|
id | Unguessable public animal id (NanoID). Use it in all subsequent animal calls. |
Response statuses
| Status | Meaning |
|---|---|
201 | Animal registered. |
409 | X-Eternity-Idempotency-Key conflict (same key, different body). |
422 | Validation error: missing nickname/is_microchip, or neither species nor species_public_id; an unknown species_public_id/breed_public_id or a breed_public_id that does not belong to the species; is_microchip=true without a valid microchip; a duplicate microchip (transponder already registered, field "transponder"); or an inline owner without email/phone or without consent.account_creation. |
/v1/partner/animals/by-identifier/{type}/{value}Lookup by a specific identifier type. Always an array.▸Auth: HMAC. Any partner key.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
type | yes | path enum | — | microchip or qr_tag. |
value | yes | path string | — | Identifier value to look up. |
Example response
{
"payload": [
{ "id": "8xK3pQzVnB7rL2qF", "species": 3,
"species_public_id": "d49c523a-d7ff-41b4-9ab6-0d1d2ce20939", "species_name": "Собака",
"breed": "Labrador", "breed_public_id": null, "breed_name": null,
"color": "black", "gender_id": 1,
"nickname": "Барсік", "microchip": "900263000123456", "qr_tag": null,
"dob": "2022-03-01", "register_date": "2026-05-30", "sterilization_status": true,
"lost_status": null, "deceased": false, "died_at": null, "status": 1,
"abilities": { "can_edit": true } }
]
}| Field | Description |
|---|---|
payload | Array of animal cards — usually one, but a value may resolve to several. |
species_public_id | Canonical species public_id — the same key you pass back as species_public_id on POST /animals. |
species_name | Species name in the request language (falls back to English, then to the Latin name). |
breed_public_id | Canonical breed public_id, or null when the animal carries only the free-text breed. |
breed_name | Canonical breed name in the request language; null when breed_public_id is null — read `breed` then. |
microchip / qr_tag | Active identifiers. |
lost_status | "active" when reported lost, otherwise null. |
deceased | True once euthanasia/death is recorded. |
Abilities
Every animal object carries an abilities object describing what the authenticated partner user may do with this animal. This is the first access flag; more will be added over time.
| Field | Description |
|---|---|
abilities.can_edit | Whether the authenticated partner user may edit this animal — update its data, add verified procedures and manage photos. True when the user registered the animal, has any relation to it (animal_user_relation), or is an active member of an organization whose registry holds it (org_animals). When false you can still record procedures: they are saved unverified until the owner approves your access request. |
Expansion (X-Eternity-Expand)
Optional header carrying a JSON array of expand keys (e.g. ["owners"]). Each requested key embeds an extra field into every animal object in the response; omit the header to get the plain card. Unknown keys are rejected with 422.
| Key | Adds | Description |
|---|---|---|
owners | owners[] | The animal's owners — the main owner plus any co-owners — each with contact data and an is_main_owner flag. Available on this partner surface only. |
"owners": [
{
"user_gid": 90231,
"public_id": "V1StGXR8Z5jd",
"has_account": true,
"email": "jane@example.com",
"phone": "+380681234567",
"display_hint": "Ja*** D.",
"language": "uk",
"country_id": "804",
"external_owner_id": "crm-4471",
"is_main_owner": true
}
]| Field | Description |
|---|---|
owners[].public_id | Stable public owner id — pass it into POST /animals owners[].public_id (API version >= 2026-07-04). |
owners[].user_gid | Legacy numeric owner id (owner-attach on older API versions). |
owners[].has_account | Whether the owner already has a usable account. |
owners[].email | Email on file for this owner (null if unknown). |
owners[].phone | Phone on file for this owner (null if unknown). |
owners[].display_hint | Masked display name (no PII). |
owners[].language | Preferred locale. |
owners[].country_id | Zero-padded ISO 3166-1 numeric code as a string (e.g. "804"). |
owners[].external_owner_id | The id YOU sent us for this person, or null if you never did. Scoped to your own integration — you never see what another partner calls the same person. |
owners[].is_main_owner | true for the main owner (animal_user_relation type main_owner); false for co-owners. |
Response statuses
| Status | Meaning |
|---|---|
200 | OK — array (possibly empty). |
/v1/partner/animals/by-identifier/{value}Lookup across all identifier types at once. Always an array.▸Auth: HMAC. Any partner key.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
value | yes | path string | — | Identifier value; searched across microchip and qr_tag. |
Example response
{ "payload": [ { "id": "8xK3pQzVnB7rL2qF", "nickname": "Барсік", "microchip": "900263000123456",
"species": 3, "species_public_id": "d49c523a-d7ff-41b4-9ab6-0d1d2ce20939",
"species_name": "Собака", "breed": "Labrador",
"breed_public_id": null, "breed_name": null } ] }| Field | Description |
|---|---|
species_public_id / breed_public_id | Canonical keys — the same ones POST /animals accepts. breed_public_id is null while the animal carries only the free-text breed. |
species_name / breed_name | Names in the request language (falling back to English, then to the Latin/canonical name). |
Abilities
Every animal object carries an abilities object describing what the authenticated partner user may do with this animal. This is the first access flag; more will be added over time.
| Field | Description |
|---|---|
abilities.can_edit | Whether the authenticated partner user may edit this animal — update its data, add verified procedures and manage photos. True when the user registered the animal, has any relation to it (animal_user_relation), or is an active member of an organization whose registry holds it (org_animals). When false you can still record procedures: they are saved unverified until the owner approves your access request. |
Expansion (X-Eternity-Expand)
Optional header carrying a JSON array of expand keys (e.g. ["owners"]). Each requested key embeds an extra field into every animal object in the response; omit the header to get the plain card. Unknown keys are rejected with 422.
| Key | Adds | Description |
|---|---|---|
owners | owners[] | The animal's owners — the main owner plus any co-owners — each with contact data and an is_main_owner flag. Available on this partner surface only. |
"owners": [
{
"user_gid": 90231,
"public_id": "V1StGXR8Z5jd",
"has_account": true,
"email": "jane@example.com",
"phone": "+380681234567",
"display_hint": "Ja*** D.",
"language": "uk",
"country_id": "804",
"external_owner_id": "crm-4471",
"is_main_owner": true
}
]| Field | Description |
|---|---|
owners[].public_id | Stable public owner id — pass it into POST /animals owners[].public_id (API version >= 2026-07-04). |
owners[].user_gid | Legacy numeric owner id (owner-attach on older API versions). |
owners[].has_account | Whether the owner already has a usable account. |
owners[].email | Email on file for this owner (null if unknown). |
owners[].phone | Phone on file for this owner (null if unknown). |
owners[].display_hint | Masked display name (no PII). |
owners[].language | Preferred locale. |
owners[].country_id | Zero-padded ISO 3166-1 numeric code as a string (e.g. "804"). |
owners[].external_owner_id | The id YOU sent us for this person, or null if you never did. Scoped to your own integration — you never see what another partner calls the same person. |
owners[].is_main_owner | true for the main owner (animal_user_relation type main_owner); false for co-owners. |
Response statuses
| Status | Meaning |
|---|---|
200 | OK — array (possibly empty). |
/v1/partner/animals/by-ownerLookup animals by owner contact. Always an array.▸Auth: HMAC. Any partner key.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
email_or_phone | yes | string | — | Exact owner email or phone (single field; email detected by format). A phone is resolved to the owner first. |
Example response
{ "payload": [ { "id": "8xK3pQzVnB7rL2qF", "nickname": "Барсік", "species": 3,
"species_public_id": "d49c523a-d7ff-41b4-9ab6-0d1d2ce20939",
"species_name": "Собака", "breed": "Labrador",
"breed_public_id": null, "breed_name": null } ] }| Field | Description |
|---|---|
species_public_id / breed_public_id | Canonical keys — the same ones POST /animals accepts. breed_public_id is null while the animal carries only the free-text breed. |
species_name / breed_name | Names in the request language (falling back to English, then to the Latin/canonical name). |
Abilities
Every animal object carries an abilities object describing what the authenticated partner user may do with this animal. This is the first access flag; more will be added over time.
| Field | Description |
|---|---|
abilities.can_edit | Whether the authenticated partner user may edit this animal — update its data, add verified procedures and manage photos. True when the user registered the animal, has any relation to it (animal_user_relation), or is an active member of an organization whose registry holds it (org_animals). When false you can still record procedures: they are saved unverified until the owner approves your access request. |
Expansion (X-Eternity-Expand)
Optional header carrying a JSON array of expand keys (e.g. ["owners"]). Each requested key embeds an extra field into every animal object in the response; omit the header to get the plain card. Unknown keys are rejected with 422.
| Key | Adds | Description |
|---|---|---|
owners | owners[] | The animal's owners — the main owner plus any co-owners — each with contact data and an is_main_owner flag. Available on this partner surface only. |
"owners": [
{
"user_gid": 90231,
"public_id": "V1StGXR8Z5jd",
"has_account": true,
"email": "jane@example.com",
"phone": "+380681234567",
"display_hint": "Ja*** D.",
"language": "uk",
"country_id": "804",
"external_owner_id": "crm-4471",
"is_main_owner": true
}
]| Field | Description |
|---|---|
owners[].public_id | Stable public owner id — pass it into POST /animals owners[].public_id (API version >= 2026-07-04). |
owners[].user_gid | Legacy numeric owner id (owner-attach on older API versions). |
owners[].has_account | Whether the owner already has a usable account. |
owners[].email | Email on file for this owner (null if unknown). |
owners[].phone | Phone on file for this owner (null if unknown). |
owners[].display_hint | Masked display name (no PII). |
owners[].language | Preferred locale. |
owners[].country_id | Zero-padded ISO 3166-1 numeric code as a string (e.g. "804"). |
owners[].external_owner_id | The id YOU sent us for this person, or null if you never did. Scoped to your own integration — you never see what another partner calls the same person. |
owners[].is_main_owner | true for the main owner (animal_user_relation type main_owner); false for co-owners. |
Response statuses
| Status | Meaning |
|---|---|
200 | OK — array (empty if the owner has no email on file for phone-only lookups). |
422 | email_or_phone is required. |
/v1/partner/animals/{id}Full animal card.▸Auth: HMAC. Any partner key.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
id | yes | path string | — | Animal public id (NanoID). |
Example response
{ "payload": { "id": "8xK3pQzVnB7rL2qF", "species": 3,
"species_public_id": "d49c523a-d7ff-41b4-9ab6-0d1d2ce20939", "species_name": "Собака",
"breed": "Labrador", "breed_public_id": null, "breed_name": null,
"nickname": "Барсік", "microchip": "900263000123456", "deceased": false,
"abilities": { "can_edit": true } } }| Field | Description |
|---|---|
species_public_id | Canonical species public_id — the same key you pass back as species_public_id on POST /animals. |
species_name | Species name in the request language (falls back to English, then to the Latin name). |
breed_public_id | Canonical breed public_id, or null when the animal carries only the free-text breed. |
breed_name | Canonical breed name in the request language; null when breed_public_id is null — read `breed` then. |
Abilities
Every animal object carries an abilities object describing what the authenticated partner user may do with this animal. This is the first access flag; more will be added over time.
| Field | Description |
|---|---|
abilities.can_edit | Whether the authenticated partner user may edit this animal — update its data, add verified procedures and manage photos. True when the user registered the animal, has any relation to it (animal_user_relation), or is an active member of an organization whose registry holds it (org_animals). When false you can still record procedures: they are saved unverified until the owner approves your access request. |
Expansion (X-Eternity-Expand)
Optional header carrying a JSON array of expand keys (e.g. ["owners"]). Each requested key embeds an extra field into every animal object in the response; omit the header to get the plain card. Unknown keys are rejected with 422.
| Key | Adds | Description |
|---|---|---|
owners | owners[] | The animal's owners — the main owner plus any co-owners — each with contact data and an is_main_owner flag. Available on this partner surface only. |
"owners": [
{
"user_gid": 90231,
"public_id": "V1StGXR8Z5jd",
"has_account": true,
"email": "jane@example.com",
"phone": "+380681234567",
"display_hint": "Ja*** D.",
"language": "uk",
"country_id": "804",
"external_owner_id": "crm-4471",
"is_main_owner": true
}
]| Field | Description |
|---|---|
owners[].public_id | Stable public owner id — pass it into POST /animals owners[].public_id (API version >= 2026-07-04). |
owners[].user_gid | Legacy numeric owner id (owner-attach on older API versions). |
owners[].has_account | Whether the owner already has a usable account. |
owners[].email | Email on file for this owner (null if unknown). |
owners[].phone | Phone on file for this owner (null if unknown). |
owners[].display_hint | Masked display name (no PII). |
owners[].language | Preferred locale. |
owners[].country_id | Zero-padded ISO 3166-1 numeric code as a string (e.g. "804"). |
owners[].external_owner_id | The id YOU sent us for this person, or null if you never did. Scoped to your own integration — you never see what another partner calls the same person. |
owners[].is_main_owner | true for the main owner (animal_user_relation type main_owner); false for co-owners. |
Response statuses
| Status | Meaning |
|---|---|
200 | OK. |
404 | Animal not found. |
/v1/partner/animals/{id}Patch mutable fields / mark deceased.▸Auth: HMAC. Owner OR vet with an active relation to the animal. · X-Eternity-Idempotency-Key required
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
nickname | no | string | — | New name. |
color | no | string | — | New color (free text — no dictionary). |
sterilization_status | no | bool | — | Set sterilized flag. |
deceased | no | bool | — | true → mark the animal dead. |
Request body (JSON)
{
"nickname": "Барсік",
"color": "black",
"sterilization_status": true,
"deceased": false
}Example response
204 No Content
Response statuses
| Status | Meaning |
|---|---|
204 | Updated. |
403 | No access to this animal — request it (POST /v1/partner/animals/{id}/access-request) and retry once the owner approves. |
409 | X-Eternity-Idempotency-Key conflict. |
422 | Validation error. |
/v1/partner/animals/{id}/access-requestAsk the animal's owner to grant you access.▸Auth: HMAC. Vet/organization key. · X-Eternity-Idempotency-Key required
Recording procedures without access raises or reuses this same request. The owner's decision covers every unverified procedure you recorded for the animal: approving gives you access and verifies them; declining removes them, and new procedures are refused until the decline expires. Approving after a decline brings the removed procedures back.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
id | yes | path string | — | Animal public id (NanoID). |
Example response
{
"payload": {
"status": "pending",
"requested_at": "2026-06-23T09:00:00+00:00",
"expires_at": "2026-06-30T09:00:00+00:00",
"retry_after_seconds": 604800
}
}| Field | Description |
|---|---|
status | "granted" — you already have access, no request was created; "pending" — awaiting the owner; "denied" — the owner declined (re-considerable until it expires). |
requested_at | When the request was raised (null when status="granted"). |
expires_at | When the request expires and you may request again (null when status="granted"). |
retry_after_seconds | Seconds until you may request the same animal again (0 once elapsed; null when status="granted"). |
Response statuses
| Status | Meaning |
|---|---|
201 | A new access request was created — the owner has been notified by email. |
200 | No new request created: you already have access (status "granted"), or an active request already exists (status "pending"/"denied") — wait retry_after_seconds before re-requesting. |
404 | Animal not found. |
/v1/partner/animals/{id}/access-requestCheck whether your access request was approved.▸Auth: HMAC. Vet/organization key.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
id | yes | path string | — | Animal public id (NanoID). |
Example response
{
"payload": {
"status": "granted",
"requested_at": "2026-06-23T09:00:00+00:00",
"expires_at": "2026-06-30T09:00:00+00:00",
"retry_after_seconds": 0
}
}| Field | Description |
|---|---|
status | "granted" — access has been approved and is active; "pending" — awaiting the owner; "denied" — declined; "none" — no active request. |
Response statuses
| Status | Meaning |
|---|---|
200 | OK. |
404 | Animal not found. |
/v1/partner/animals/{id}/proceduresRecord procedures; opens a visit — or, without access to the animal, records them unverified. Single object or array.▸Auth: HMAC. Vet/organization key. · X-Eternity-Idempotency-Key required
Without access to the animal (abilities.can_edit is false) the procedures are still recorded, but unverified: no visit is opened (appointment_id is null), nothing on the animal changes yet (a chip, tag, sterilization or death waits for the owner), and an access request for your key's organization is raised or reused. The owner is emailed and decides once for every unverified procedure you recorded for the animal: approving gives you access and verifies them — their changes to the animal are applied then; declining removes them. While a decline is in force, new procedures for the animal are refused with 403.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
(body) | yes | object | array | — | A single procedure object or an array of them (≤100). |
type | yes | int | procedure_types | Procedure catalogue id: 10 vaccination, 20 rabies vaccination, 30 transponder identification, 40 token identification, 50 deworming, 60 sterilization, 70 euthanasia / death certification. |
occurred_at | yes | datetime | — | When performed (ISO 8601). |
summary | no | string | — | Free-text note. |
revaccination_date | no | date | — | Override next-vaccination date (vaccinations). |
type_specific_payload | no | object | — | Per-type fields — validated server-side: 10/20 → {vaccine_name*, batch_number*}; 30 → {transponder_number* — 15 digits}; 40 → {token_number*}; 50 → {drug*, batch_number?, dose?}; 60 → {method?, anesthesia_type?}; 70 → {reason*, death_date*}. For type 30: if the animal already carries a microchip, it is kept — the new number is stored as an additional identifier (other_identifiers type 11) instead of replacing the chip. |
Request body (JSON)
[
{
"type": 10,
"occurred_at": "2026-05-30T08:00:00+00:00",
"summary": "Annual shot",
"revaccination_date": "2027-05-30",
"type_specific_payload": {
"vaccine_name": "Nobivac",
"batch_number": "A123"
}
},
{
"type": 30,
"occurred_at": "2026-05-30T08:05:00+00:00",
"type_specific_payload": {
"transponder_number": "900263000123456"
}
}
]Example response
{
"payload": {
"appointment_id": null,
"procedures": [
{ "id": 99001, "animal_id": "8xK3pQzVnB7rL2qF", "visit_id": null, "type": 10,
"occurred_at": "2026-05-30T08:00:00+00:00", "summary": "Annual shot", "revaccination_date": "2027-05-30",
"type_specific_payload": { "vaccine_name": "Nobivac", "batch_number": "A123" },
"verification_status": "unverified", "verified_at": null }
],
"access_request": {
"status": "pending",
"requested_at": "2026-05-30T08:00:00+00:00",
"expires_at": "2026-06-06T08:00:00+00:00",
"retry_after_seconds": 604800
}
}
}| Field | Description |
|---|---|
appointment_id | The visit opened for this batch; null when the batch was recorded unverified (no access). |
procedures[] | The recorded procedures — same card as GET list/show. |
procedures[].id | Procedure record id. |
procedures[].type | Numeric type id (procedure_types dictionary). |
procedures[].type_specific_payload | Stored type-specific payload. |
procedures[].verification_status | "verified" — recorded with access to the animal, or confirmed by the owner; "unverified" — recorded without access and awaiting the owner's decision. |
procedures[].verified_at | When the owner confirmed the procedure; null when it was recorded with access or is still unverified. |
access_request | The access request an unverified batch was recorded under — the same object as GET /v1/partner/animals/{id}/access-request. null when you have access to the animal. |
Response statuses
| Status | Meaning |
|---|---|
201 | Recorded: verified with a visit opened, or — without access — unverified under an access request. |
403 | The owner declined your access request for this animal and the decline is still in force — new procedures are refused until it expires. |
404 | Animal not found. |
409 | X-Eternity-Idempotency-Key conflict, or a QR tag (type 40) already attached to an animal. |
422 | Unsupported type, missing occurred_at, or missing type-specific fields. |
/v1/partner/animals/{id}/proceduresList an animal's procedures. Always an array.▸Auth: HMAC. Vet/organization key.
Lists verified procedures, plus the unverified ones you recorded yourself. Other vets' unverified procedures are not shown.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
type | no | int | procedure_types | Filter by procedure catalogue id. |
since | no | datetime | — | Only on/after this time. |
until | no | datetime | — | Only on/before this time. |
Example response
{
"payload": [
{ "id": 99001, "animal_id": "8xK3pQzVnB7rL2qF", "visit_id": 7741, "type": 10,
"occurred_at": "2026-05-30T08:00:00+00:00", "summary": null, "revaccination_date": "2027-05-30",
"type_specific_payload": { "vaccine_name": "Nobivac", "batch_number": "A123" },
"verification_status": "verified", "verified_at": null }
]
}| Field | Description |
|---|---|
type | Procedure catalogue id (procedure_types dictionary). |
visit_id | Appointment the procedure was recorded under. |
type_specific_payload | Per-type fields. |
verification_status | "verified" — recorded with access to the animal, or confirmed by the owner; "unverified" — recorded without access and awaiting the owner's decision. |
verified_at | When the owner confirmed the procedure; null when it was recorded with access or is still unverified. |
Response statuses
| Status | Meaning |
|---|---|
200 | OK — array (possibly empty). |
404 | Animal not found. |
/v1/partner/procedures/{id}Single procedure.▸Auth: HMAC. Vet/organization key.
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
id | yes | path int | — | Procedure record id. |
Example response
{
"payload": {
"id": 99001, "animal_id": "8xK3pQzVnB7rL2qF", "visit_id": 7741, "type": 10,
"occurred_at": "2026-05-30T08:00:00+00:00", "summary": null, "revaccination_date": "2027-05-30",
"type_specific_payload": { "vaccine_name": "Nobivac", "batch_number": "A123" },
"verification_status": "verified", "verified_at": "2026-05-31T10:00:00+00:00"
}
}Response statuses
| Status | Meaning |
|---|---|
200 | OK. |
404 | Procedure not found — including another vet's unverified procedure. |
/v1/partner/animals/{id}/photosUpload a photo (multipart). Owner or vet with relation.▸Auth: HMAC + multipart/form-data. Owner OR vet with relation. · X-Eternity-Idempotency-Key required
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
file | yes | file | — | Image file. Max 8 MB per photo (config partner.photos.max_single_mb). |
kind | no | enum | — | avatar | gallery | nose_print. avatar sets the main photo. Default gallery. |
Example response
{ "payload": { "id": 33015 } }| Field | Description |
|---|---|
id | New photo id. |
Response statuses
| Status | Meaning |
|---|---|
201 | Uploaded. |
403 | No access to this animal — request it (POST /v1/partner/animals/{id}/access-request) and retry once the owner approves. |
422 | Invalid file or larger than 8 MB (reduce size / lower quality). |
413 | Whole request exceeds 15 MB. |
/v1/partner/animals/{id}/photos/{photoId}Soft-delete a photo.▸Auth: HMAC. Owner OR vet with relation. · X-Eternity-Idempotency-Key required
Request fields
| Field | Req. | Type | Dictionary | Description |
|---|---|---|---|---|
id | yes | path string | — | Animal public id (NanoID). |
photoId | yes | path int | — | Photo id. |
Example response
204 No Content
Response statuses
| Status | Meaning |
|---|---|
204 | Deleted. |
403 | No access to this animal — request it (POST /v1/partner/animals/{id}/access-request) and retry once the owner approves. |