Перейти к содержимому

Публичный API

Стабильный read-only API для внешних сайтов и ботов. Он отдаёт карточки помощи, видимые всем, — ровно то, что видит анонимный посетитель ленты Paws Help. Черновики и скрытые карточки в API не попадают. Список по умолчанию включает активные, приостановленные и закрытые карточки; для блока «кому помочь сейчас» передавайте status=active.

Контракт не зависит от внутренней схемы базы данных: её изменения не ломают потребителей. Готовый блок для сайта без программирования описан на странице «Блок для сайта партнёра».

Base URL https://serbia.paws-help.com
Версия v1 (в пути: /api/v1/...)
Формат JSON, UTF-8
CORS Точный разрешённый Origin в Access-Control-Allow-Origin, Vary: Origin

Используйте только адрес https://serbia.paws-help.com/api/v1/cards. Старый адрес на paws-help.com/api/v1/cards отключён и отвечает 410 с полем url, в котором указан правильный адрес.

Есть два режима доступа. Оба нужно согласовать с администратором Paws Help.

Запросы с вашего сервера передают API-ключ одним из двух способов:

Authorization: Bearer <ВАШ_КЛЮЧ>
X-API-Key: <ВАШ_КЛЮЧ>
  • Ключ выдаёт администратор Paws Help по запросу.
  • Храните ключ только в серверном окружении: не в HTML, JavaScript для браузера, репозитории или localStorage.
  • Неверный или отозванный ключ → 401, даже если запрос пришёл с разрешённого сайта.

Браузер на сайте партнёра — по адресу сайта

Заголовок раздела «Браузер на сайте партнёра — по адресу сайта»

Статический сайт может читать API прямо из браузера без ключа, если его адрес (Origin) внесён в список разрешённых. Сообщите администратору точный адрес сайта, например https://partner.example.

  • Адрес указывается целиком: схема и хост, без пути и завершающего /. Маски вида *.pages.dev не принимаются.
  • Запросы делайте с credentials: 'omit', без ключа и cookies. Заголовок Origin браузер ставит сам.
  • Страница, открытая как файл (file://), не работает: у неё нет адреса.
  • Разрешённому сайту API возвращает CORS-заголовки и на ошибки (401, 404, 429, 5xx), а через Access-Control-Expose-Headers: Retry-After даёт прочитать задержку после 429.
Метод Путь Назначение
GET /api/v1/cards Список карточек (фильтры и пагинация)
GET /api/v1/cards/:id Одна карточка по id
OPTIONS оба пути CORS preflight → 204

Любой другой метод (POST, PUT, DELETE, …) → 405 { "error": "method not allowed" }.

Список карточек, новые сверху.

Query-параметры (все необязательны; невалидные значения тихо игнорируются):

Параметр Значения По умолчанию
type foster | adoption | fundraise все типы
species cat | dog | other все виды
status подмножество active,paused,closed через запятую все видимые
limit целое 1..100 50
offset целое ≥ 0 0
lang ru | sr | en — язык текстового контента ru

Ответ 200:

{
"cards": [ /* PublicCard[] */ ],
"total": 123, // всего карточек под текущим фильтром
"limit": 50,
"offset": 0,
"lang": "ru" // язык контента в этом ответе
}

Постранично: запрашивайте offset = 0, 50, 100, …, пока offset + cards.length < total. total и cards считают карточки помощи, а не уникальных животных: у одного питомца может быть несколько карточек. Как их сгруппировать — ниже, в разделе про subjectId.

Сортировка — created_at DESC, id DESC. Пагинация идёт по живой ленте: между запросами страниц новые карточки могут сдвигать позиции.

name, description, age и notes по умолчанию отдаются на русском: карточки заводятся на русском. С ?lang=sr (сербский, латиница) или ?lang=en эти поля заменяются сохранённым переводом. Поле без актуального перевода остаётся русским. Изменение исходного текста сбрасывает устаревший перевод до следующего перевода.

Структурные поля (type, status, species, sex) и суммы не переводятся. Города из справочника переводятся, неизвестные кириллические названия транслитерируются. url сохраняет выбранный язык. Тот же ?lang= работает и на /cards/:id.

Одна карточка по UUID.

{ "card": { /* PublicCard */ }, "lang": "sr" }

Если карточки нет, она скрыта или это черновик → 404 { "error": "not found" }. Для внешнего сервиса все три случая неотличимы.

Поля fundraise, foster и adoption приходят только для своего type.

{
"id": "f7c1...-uuid", // UUID карточки помощи
"subjectId": "a8b2...-uuid", // UUID питомца или null для старой одиночной карточки
"type": "foster | adoption | fundraise",
"status": "active | paused | closed",
"name": "Барсик", // локализуется ?lang
"species": "cat | dog | other",
"sex": "male | female | unknown",
"age": "2 года", // строка или null; локализуется ?lang
"description": "Полный текст описания карточки",
"cities": [
{ "id": "uuid", "name": "Белград" } // при lang=sr → Beograd
],
"coverUrl": "https://…/cover.jpg", // абсолютный URL обложки или null
"media": [
{ "url": "https://…/photo.jpg", "type": "photo", "thumbUrl": "https://…/thumb.jpg", "focusX": 0.5, "focusY": 0.5 },
{ "url": "https://…/clip.mp4", "type": "video" } // thumbUrl может отсутствовать
],
"curator": { "name": "Аня" },
"url": "https://serbia.paws-help.com/animals/f7c1...-uuid?lang=ru",
"createdAt": "2026-06-01T10:00:00Z", // ISO-8601 или null
// только при type=fundraise:
"fundraise": { "reason": "surgery | transport | medicine | other", "goal": 1000, "raised": 250 },
// только при type=foster:
"foster": { "from": "2026-06-01", "until": null, "pricePerDay": 12.5, "notes": "только в квартиру" },
// только при type=adoption:
"adoption": { "notes": "Отдаём в добрые руки" }
}
  • coverUrl равен media[0].url: обложка идёт первой. null, если фото нет.
  • focusX / focusY — необязательная точка фокуса обложки в диапазоне 0..1. Её можно передать в CSS object-position, чтобы кадрировать фото так же, как в приложении.
  • url — готовая ссылка на карточку в Paws Help, всегда на https://serbia.paws-help.com и с языком ответа.
  • Суммы (goal, raised, pricePerDay) — числа в сербских динарах (RSD).
  • Донаты, чеки и контакты кураторов в API не входят.

Чтобы показать уникальных животных, сначала загрузите нужные страницы с status=active, затем сгруппируйте по card.subjectId ?? card.id. Не группируйте по имени или по null: независимые старые карточки должны остаться разными животными. Сохраняйте все действия помощи внутри группы.

// allCards — карточки со всех нужных страниц; каждая группа хранит все действия.
const animals = new Map();
for (const card of allCards) {
const key = card.subjectId ?? card.id;
const actions = animals.get(key) ?? [];
actions.push(card);
animals.set(key, actions);
}

Число групп может быть меньше total; для пагинации по-прежнему используйте total.

Код Когда
200 Успех
204 Ответ на OPTIONS (preflight)
401 Нет разрешённого Origin или ключа, либо ключ неверен или отозван
404 Карточка не найдена, скрыта или это черновик
405 Неподдерживаемый метод
410 Старый адрес на paws-help.com отключён; поле url содержит правильный
429 Превышен лимит запросов; подождите Retry-After секунд
500 Непредвиденная внутренняя ошибка
503 API временно недоступен

Тело ошибки: { "error": "<текст>" }. Для 500 и 503 в теле есть requestId — укажите его, если пишете администратору. Ошибки отдаются с Cache-Control: no-store.

В серверных примерах KEY — выданный вам ключ.

Окно терминала
KEY="phk_xxx"
BASE="https://serbia.paws-help.com/api/v1"
# Активные сборы, первые 20
curl -s -H "Authorization: Bearer $KEY" \
"$BASE/cards?type=fundraise&status=active&limit=20"
# Кошки на передержку, вторая страница (50..99)
curl -s -H "X-API-Key: $KEY" \
"$BASE/cards?type=foster&species=cat&limit=50&offset=50"
# Одна карточка
curl -s -H "Authorization: Bearer $KEY" \
"$BASE/cards/f7c1aaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
# Список на сербском
curl -s -H "Authorization: Bearer $KEY" \
"$BASE/cards?status=active&lang=sr"

Ключ и cookies не нужны, Origin браузер передаст сам.

const response = await fetch(
'https://serbia.paws-help.com/api/v1/cards?status=active&limit=100&lang=sr',
{ credentials: 'omit', headers: { Accept: 'application/json' } },
);
if (!response.ok) throw new Error(`Cards API ${response.status}`);
const { cards, total, limit, offset } = await response.json();
// Это карточки помощи: у одного животного их может быть несколько.
const BASE = 'https://serbia.paws-help.com/api/v1';
const KEY = process.env.PAWS_API_KEY; // не вшивайте ключ в публичный бандл
async function listCards(params = {}) {
const qs = new URLSearchParams(params).toString();
const res = await fetch(`${BASE}/cards?${qs}`, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) throw new Error(`API ${res.status}: ${(await res.json()).error}`);
return res.json(); // { cards, total, limit, offset, lang }
}
// Все активные карточки постранично
async function fetchAll() {
const all = [];
let offset = 0;
const limit = 100;
for (;;) {
const { cards, total } = await listCards({ status: 'active', limit, offset });
all.push(...cards);
offset += cards.length;
if (cards.length === 0 || offset >= total) break;
}
return all;
}
import requests
BASE = "https://serbia.paws-help.com/api/v1"
KEY = "phk_xxx"
HEADERS = {"Authorization": f"Bearer {KEY}"}
def list_cards(**params):
r = requests.get(f"{BASE}/cards", headers=HEADERS, params=params, timeout=15)
r.raise_for_status()
return r.json() # {"cards": [...], "total": N, "limit": L, "offset": O, "lang": "ru"}
def fetch_all(**filters):
out, offset, limit = [], 0, 100
while True:
data = list_cards(limit=limit, offset=offset, **filters)
out.extend(data["cards"])
offset += len(data["cards"])
if not data["cards"] or offset >= data["total"]:
break
return out
for c in fetch_all(status="active", type="fundraise"):
print(c["name"], c["url"], c.get("fundraise"))
  • Список карточек кэшируется на сервере на 30 секунд; одна карточка не кэшируется. Всем ответам отдаётся Cache-Control: no-store.
  • Рассчитывайте, что изменения появляются в API примерно в течение минуты. Актуальное состояние всегда видно в самой карточке Paws Help.
  • Лимиты: 240 запросов в минуту с одного IP и дополнительно 60 запросов в минуту на один ключ. Лимиты считаются отдельно в каждой точке присутствия Cloudflare и не являются строгой глобальной квотой.
  • При 429 подождите Retry-After секунд и не повторяйте запрос чаще.
  • Обновляйте видимый блок не чаще раза в 30 секунд и убирайте устаревшие данные.
  • Поля type, status, species и sex могут получить новые значения в будущем: обрабатывайте незнакомые значения без ошибок.
  • Для перехода на карточку используйте готовое поле url.
  • Ссылки на медиа (coverUrl, media[].url, thumbUrl) абсолютные, их можно ставить прямо в <img>.
  • Нужны все языки — делайте отдельный запрос на каждый lang.