TrofeyАяй → Вебхуки
Аяй

Вебхуки: находки в твой сервер

Единственная часть этой документации, которая про платный продукт и про твой аккаунт, а не про открытые данные.

← Всё об открытых данных

Когда нужны не числа, а находки

Всё выше — открытое и безличное: цены, которые и так стоят на страницах, без ключа и без аккаунта. Вебхук — другая вещь: персональная и платная. Это тот же алерт, который аккаунт уже получает в Telegram, отправленный POST-ом на его собственный адрес. Те же находки, тот же аккаунт, те же оплаченные позиции — меняется только то, куда прилетает.

Отсюда главное ограничение, и оно намеренное: вебхук не даёт доступа ни к чему новому. Нет живой позиции — нет алертов вообще, значит вебхуку нечего отправлять. Бесплатного обхода платной доставки здесь нет.

Включается в кабинете: адрес (только https), канал доставки — Telegram, вебхук или оба, и секрет, который можно показать ещё раз или сменить. Сохранение адреса — ещё не включение: мы шлём на него событие ping, и доставка включается только после ответа 2xx. Иначе первым тестом интеграции стала бы первая настоящая находка — а она живёт минуты.

Что прилетает

POST, Content-Type: application/json, тело — UTF-8 JSON с отсортированными ключами и без пробелов. Это те же факты, что на карточке, и не больше: контактов продавца тут нет — мы их не храним. Событие ping приезжает на тот же адрес и без listing, так что различай события по полю event.

{  "event": "alert",  "event_id": "148217",  "item": {    "battery_pct": 92,    "bucket_key": "iphone-13|128|used|clean",    "category": "phones",    "condition": "used",    "lock_status": "clean",    "model_key": "iphone-13",    "ram_gb": null,    "storage_gb": 128  },  "listing": {    "city": "Київ",    "currency": "UAH",    "photo_url": "https://.../image.jpg",    "posted_at": "2026-09-18T07:06:00Z",    "price_uah": 10000,    "title": "iPhone 13 128GB",    "url": "https://www.olx.ua/d/uk/obyavlenie/..."  },  "market": {    "delta_pct": 23.1,    "delta_uah": 3000,    "median_uah": 13000  },  "reason": "new",  "sent_at": "2026-09-18T07:11:04.812345Z",  "silent": false}

bucket_key есть, но разбирать его строкой не надо: все его измерения лежат отдельными полями в item. Время — ISO-8601 в UTC, и у секунд может быть дробная часть, так что разбирай его ISO-парсером, а не своим шаблоном на секунды.

Заголовки

POST /your/endpoint HTTP/1.1Content-Type: application/jsonUser-Agent: Trofey-Webhook/1X-Trofey-Event-Id: 148217X-Trofey-Timestamp: 1758179464X-Trofey-Delivery-Attempt: 1X-Trofey-Signature: sha256=8f1c...e2
X-Trofey-Event-Id
идентификатор события, он же event_id в теле
X-Trofey-Timestamp
время отправки, unix-секунды
X-Trofey-Delivery-Attempt
номер попытки, от 1
X-Trofey-Signature
sha256= и hex: HMAC-SHA256 над строкой из времени, точки и тела

Подпись

Подписываются именно те байты, что приехали. Возьми сырое тело до любого парсинга, составь строку {timestamp}.{body}, посчитай HMAC-SHA256 своим секретом и сравни с X-Trofey-Signature в постоянном времени. И отбрось запрос, если время далеко от «сейчас» (разумно — 5 минут): время входит в подписанную строку именно для этого, ведь без проверки времени подсмотренную доставку можно повторять вечно.

Готовый приёмник — examples/webhooks/: Python, Node и PHP, без единой зависимости. Вот часть, которая принимает решение; мы гоняем именно её против настоящих подписанных доставок:

import hashlib, hmac, timeCLOCK_SKEW_SEC = 300def verify(body: bytes, timestamp: str, signature: str, secret: str) -> str | None:    """None when the delivery is genuine and fresh; otherwise why it is not.    `body` must be the bytes as they ARRIVED: json.loads() then json.dumps()    produces different bytes and a signature that can never match."""    if not timestamp or not signature:        return "missing signature headers"    try:        sent_at = int(timestamp)    except ValueError:        return "unreadable timestamp"    if abs(time.time() - sent_at) > CLOCK_SKEW_SEC:        return "stale timestamp"    mine = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256)    if not hmac.compare_digest(f"sha256={mine.hexdigest()}", signature):        return "bad signature"    return None

Секрет выводится, а не хранится: в базе лежит только номер версии, поэтому показать его второй раз можно всегда, а ротация — это +1 к версии, после которой старые подписи сразу перестают быть действительными.

Доставка

Доставка — как минимум раз, а не ровно раз. Процесс, умерший между POST-ом и отметкой в очереди, повторит это одно событие, поэтому event_id стабилен и уникален — дедуплицируй по нему. Бери его из тела, а не из заголовка: подписаны только тело и время.

Вопросы и ответы

Что будет, если мой сервер не ответит?

Повторим четыре раза — через 30, 60, 120 и 240 секунд. Если не выйдет и тогда, письмо становится мёртвым. После трёх мёртвых доставку на адрес выключаем и пишем об этом в Telegram — находки идут туда, ничего не теряется.

Как убедиться, что запрос действительно от вас?

Каждая доставка подписана: заголовок несёт HMAC-SHA256 от «временная метка.тело» на твоём секрете. Проверяй подпись до того, как читать тело — готовый код есть на этой странице.

Может ли одна находка прийти дважды?

Да. Доставка «как минимум один раз», так что повтор возможен после сбоя сети. Заголовок с идентификатором события — ключ, по которому такой повтор отсеивается на твоей стороне.