Публичный 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" }.
GET /api/v1/cards
Заголовок раздела «GET /api/v1/cards»Список карточек, новые сверху.
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. Пагинация идёт по живой ленте: между
запросами страниц новые карточки могут сдвигать позиции.
Язык контента (?lang)
Заголовок раздела «Язык контента (?lang)»name, description, age и notes по умолчанию отдаются на русском: карточки
заводятся на русском. С ?lang=sr (сербский, латиница) или ?lang=en эти поля
заменяются сохранённым переводом. Поле без актуального перевода остаётся
русским. Изменение исходного текста сбрасывает устаревший перевод до следующего
перевода.
Структурные поля (type, status, species, sex) и суммы не переводятся.
Города из справочника переводятся, неизвестные кириллические названия
транслитерируются. url сохраняет выбранный язык. Тот же ?lang= работает
и на /cards/:id.
GET /api/v1/cards/:id
Заголовок раздела «GET /api/v1/cards/:id»Одна карточка по UUID.
{ "card": { /* PublicCard */ }, "lang": "sr" }Если карточки нет, она скрыта или это черновик → 404 { "error": "not found" }.
Для внешнего сервиса все три случая неотличимы.
Модель PublicCard
Заголовок раздела «Модель PublicCard»Поля 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. Её можно передать в CSSobject-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"
# Активные сборы, первые 20curl -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"JavaScript в браузере (разрешённый сайт)
Заголовок раздела «JavaScript в браузере (разрешённый сайт)»Ключ и 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();// Это карточки помощи: у одного животного их может быть несколько.JavaScript на сервере (Node 22+)
Заголовок раздела «JavaScript на сервере (Node 22+)»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;}Python (requests)
Заголовок раздела «Python (requests)»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.