Вебхуки - это та часть API сокращателя ссылок, которую внедряют все, но почти никто не делает это хорошо. Сложность заключается не в кодировании - полезная нагрузка представляет собой объект JSON, - а в операционных деталях: проверка подписи, политика повторных попыток, идемпотентность, гарантии доставки и поведение системы, когда конечная точка подписчика недоступна в течение двух дней.
В этой статье задокументировано каждое событие вебхука, которое отправляет Elido, форматы полезной нагрузки, кривая повторных попыток и схема подписи. В быстром старте по API + SDK сокращателя ссылок описана входящая часть API; здесь же рассматривается исходящая.
12 типов событий
Elido генерирует 12 типов событий вебхуков, сгруппированных в три семейства:
События кликов и трафика: click, bio.click, qr.scan, conversion. Они срабатывают при каждом перенаправлении или сканировании после небольшой задержки в очереди (описано ниже).
События жизненного цикла: link.created, link.updated, link.deleted, bio.published. Они срабатывают на уровне API при изменении соответствующей записи.
События агрегации и операций: daily.summary, campaign.ended, alert.threshold_exceeded, quota.warning. Они срабатывают по расписанию или при превышении порогового значения.
Подписчик регистрирует вебхук через POST /v1/webhooks, указывая целевой URL и массив событий, которые необходимо доставлять. Полный запрос на подписку:
POST /v1/webhooks
Content-Type: application/json
Authorization: Bearer <api-key>
{
"url": "https://example.com/webhooks/elido",
"events": ["click", "conversion", "link.created"],
"secret": "whsec_<32-byte-base64>",
"active": true
}
secret - это ключ HMAC, используемый для подписи исходящих запросов. Он непрозрачен для Elido; мы никогда не логируем и не отображаем его после ответа на запрос создания.
Формат события click
По объему это событие, которое интересует вас больше всего. Каждое перенаправление через любую короткую ссылку создает одно событие click после того, как перенаправление было обслужено клиенту. Формат:
{
"id": "evt_2g8kFqJxYwPaZcvAm3HsTr",
"type": "click",
"created_at": "2026-05-22T14:32:18.847Z",
"data": {
"link_id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
"short_url": "https://elido.me/abc123",
"destination_url": "https://shop.example.com/spring",
"click_id": "clk_2g8kFqJxYwPaZcvAm3HsTr",
"ip_prefix": "203.0.113.0/24",
"country": "DE",
"city_geoname_id": 2950159,
"user_agent_family": "Chrome 124",
"device_type": "mobile",
"os_family": "iOS 17.5",
"referrer": "https://www.google.com",
"utm_source": "newsletter",
"utm_medium": "email",
"utm_campaign": "spring-2026",
"utm_term": null,
"utm_content": null
},
"workspace_id": "ws_12"
}
Несколько деталей, которые стоит выделить:
ip_prefix, а неip. Мы сохраняем префикс сети /24 (IPv4) или /48 (IPv6), а не полный адрес. В статье GDPR для сокращателей ссылок объясняется почему; на практике это означает, что подписчик получает достаточную географическую точность для аналитики без ответственности за обработку полных IP-адресов как персональных данных.city_geoname_id, а неcity_name. Идентификатор GeoNames стабилен для всех локалей; название города варьируется. Если вам нужно локализованное название, найдите идентификатор в дампе GeoNames.org один раз и кэшируйте результат.user_agent_family, а не полная строка UA. Мы удаляем полный UA при приеме (это данные с высокой энтропией для снятия отпечатков); семейство - это браузер+мажорная версия, которые остаются.
Задержка между перенаправлением клиенту и срабатыванием вебхука обычно составляет от 200 мс до 2 с. События кликов сначала проходят через наш поток событий, агрегируются для аналитики, а затем воркер fan-out отправляет вебхуки. Это тот же конвейер, который питает аналитику дашборда - в статье Fire-and-forget прием кликов описана механика очереди.
Формат события conversion
События conversion срабатывают, когда клик сопоставляется с последующей конверсией - покупкой, регистрацией, лид-формой или чем-то еще, что вы подключили к конвейеру пересылки конверсий.
{
"id": "evt_2g8mPzKlYxRcBnQvFt5HwS",
"type": "conversion",
"created_at": "2026-05-22T14:38:42.193Z",
"data": {
"click_id": "clk_2g8kFqJxYwPaZcvAm3HsTr",
"link_id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
"conversion_id": "cnv_2g8mPzKlYxRcBnQvFt5HwS",
"value": 49.5,
"currency": "EUR",
"event_name": "purchase",
"product_id": "sku_42",
"metadata": {
"order_id": "ord_12345",
"is_new_customer": true
},
"attribution_window_minutes": 6,
"forwarded_to": ["meta_capi", "ga4_mp"]
},
"workspace_id": "ws_12"
}
click_id ссылается на исходное событие клика; вы можете объединить их на стороне сервера, чтобы восстановить путь «клик - конверсия». attribution_window_minutes - это время, прошедшее между кликом и конверсией, что полезно для моделирования атрибуции.
Массив forwarded_to сообщает, в какие пиксели платформ Elido уже отправил эту конверсию. Если ваш подписчик передает конверсии в ваше собственное хранилище данных, вы можете использовать это, чтобы избежать двойного учета в вашей аналитике.
Формат события link.created
События жизненного цикла имеют более «тонкий» формат - только ресурс и субъект:
{
"id": "evt_2g8mPzKlYxRcBnQvFt5HwS",
"type": "link.created",
"created_at": "2026-05-22T14:38:42.193Z",
"data": {
"link": {
"id": "lnk_2g3jQpRyXz4Mn8VbF7Hkl",
"slug": "abc123",
"short_url": "https://elido.me/abc123",
"destination_url": "https://shop.example.com/spring",
"domain": "elido.me",
"tags": ["spring-2026", "newsletter"],
"created_at": "2026-05-22T14:38:42.193Z",
"created_by": "usr_42"
}
},
"workspace_id": "ws_12"
}
link.updated включает снимок previous наряду с новым состоянием; link.deleted включает конечное состояние ссылки на момент удаления. Полная схема находится в руководстве по эксплуатации /docs/guides/conversion-forwarding.
Проверка подписи
Каждый запрос вебхука включает три HTTP-заголовка:
Elido-Signature: t=1716392538,v1=4f3b2e1d9c8a7b6f5e4d3c2b1a0f9e8d7c6b5a49382716054 ...
Elido-Webhook-Id: evt_2g8kFqJxYwPaZcvAm3HsTr
Elido-Delivery-Attempt: 1
Схема подписи следует модели Stripe: HMAC-SHA256 над {timestamp}.{body} с использованием секрета вебхука. Префикс v1= - это версия алгоритма подписи; новые версии алгоритма добавляются до того, как они станут стандартными, поэтому подписчики могут проверять несколько версий одновременно.
Проверка в Go:
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
)
func verify(sigHeader, body, secret string) bool {
parts := strings.Split(sigHeader, ",")
var t int64
var v1 string
for _, p := range parts {
kv := strings.SplitN(p, "=", 2)
if len(kv) != 2 { continue }
switch kv[0] {
case "t":
fmt.Sscanf(kv[1], "%d", &t)
case "v1":
v1 = kv[1]
}
}
if time.Since(time.Unix(t, 0)) > 5*time.Minute {
return false // reject stale requests
}
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.%s", t, body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(v1))
}
Проверка на «свежесть» в 5 минут - это то, о чем забывает большинство подписчиков. Без нее возможна атака повторного воспроизведения (replay attack) - злоумышленник, перехвативший валидный запрос, отправляет его позже, и он принимается, так как подпись все еще валидна. С проверкой по метке времени запрос принимается только в течение 5-минутного окна с момента отправки Elido.
Спецификация подписи задокументирована в шпаргалке OWASP по безопасности вебхуков; мы не изобретали этот паттерн, мы просто реализовали его.
Политика повторных попыток
Это та часть, где большинство реализаций вебхуков становятся небрежными.
Вебхук срабатывает один раз при успешном сценарии: подписчик возвращает 2xx, диспетчер фиксирует успех, событие завершено. Более сложные случаи - это ответы не 2xx, сетевые ошибки и медленные ответы подписчиков.
График повторных попыток Elido:
| Попытка | Задержка после предыдущей | Накоплено | Статус |
|---|---|---|---|
| 1 | - | 0 | начальная |
| 2 | 1с | 1с | первая повторная |
| 3 | 30с | 31с | |
| 4 | 5м | 5м 31с | |
| 5 | 1ч | 1ч 5м 31с | |
| 6 | 6ч | 7ч 5м 31с | |
| 7 | 24ч | 31ч 5м 31с | финальная |
После 7-й попытки (~31 час после первой) диспетчер сдается и генерирует внутреннее событие webhook.failed. Конечная точка подписчика помечается как деградировавшая после трех последовательных сбоев в любых событиях; для деградировавших подписок бюджет повторных попыток сокращается на 24 часа. После 50 последовательных сбоев подписка приостанавливается, а владелец рабочего пространства получает уведомление.
Поведение повторных попыток учитывает заголовки Retry-After от подписчика. Если ваша конечная точка ограничивает Elido (возвращая 429 с Retry-After: 120), следующая попытка будет ждать 120 секунд вместо стандартных 30 секунд по графику.
Отсутствие ответа в течение 10 секунд рассматривается как тайм-аут и засчитывается как неудачная попытка. Бюджет в 10 секунд сделан намеренно щедрым - он покрывает задержку «холодного старта» для serverless-подписчиков, но если ваша конечная точка регулярно отвечает дольше 5 секунд, исправьте это в первую очередь; это обойдется вам в объеме повторных попыток.
Идемпотентность
Подписчики могут получать одно и то же событие более одного раза.
Это не ошибка; это следствие того, как работает распределенная доставка сообщений. Если подписчик возвращает 504, потому что их бэкенд был медленным, но в итоге обработал событие, диспетчер повторит попытку; подписчик получит его дважды и может обработать дважды. То же самое событие может сработать дважды, если диспетчер упадет в процессе доставки и событие будет поставлено в очередь снова.
Способ решения: каждое событие имеет уникальный id (префикс evt_…). Подписчикам следует сохранять идентификаторы, которые они уже обработали (подойдет небольшая таблица ключ-значение; TTL 14 дней покрывает окно повторных попыток с запасом), и пропускать события, чей идентификатор они уже видели.
CREATE TABLE webhook_processed_events (
event_id TEXT PRIMARY KEY,
received_at TIMESTAMPTZ DEFAULT now()
);
-- in your handler:
INSERT INTO webhook_processed_events (event_id) VALUES ($1)
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
-- if the RETURNING is empty, you've already processed this event
ON CONFLICT DO NOTHING - это дешевая проверка на идемпотентность. Если вставка возвращает строку, это событие вы видите впервые; если ничего не возвращает - вы его уже обработали.
Для высоконагруженных подписчиков (>1 тыс. событий/сек) выделенный Redis SETNX с TTL работает аналогичным образом с меньшими затратами, чем строка в Postgres.
Порядок доставки
Нет глобальной гарантии порядка. События с одним и тем же link_id отправляются в порядке подачи, но события от разных ссылок могут приходить вперемешку. Событие click во время T+0 и событие conversion во время T+10мс могут прийти к подписчику в любом порядке в зависимости от состояния пула воркеров.
Метки времени created_at являются авторитетными для порядка. Если вашему подписчику необходим строгий порядок, сортируйте по created_at на стороне сервера перед обработкой.
В частности, для пути клик → конверсия: событие конверсии всегда ссылается на click_id события клика, поэтому вы можете объединить их на стороне сервера, даже если они приходят не по порядку.
Вебхуки против опроса (polling) - компромисс
В статье вебхуки против опроса для отслеживания кликов это подробно описано. Краткий ответ: вебхуки - правильный паттерн, когда (а) вам нужна низкая задержка при получении событий (<5 секунд), и (б) ваш подписчик доступен из публичного интернета по TLS. Опрос - правильный паттерн, когда (а) вам не нужно реальное время, (б) вы контролируете хранилище данных и просто хотите ежедневную/ежечасную пакетную загрузку, или (в) ваш подписчик находится в сети, которая не принимает входящий трафик.
Для большинства команд вебхуки - верное решение. Кривая повторных попыток изящно обрабатывает временные сбои; схема подписи обеспечивает безопасность; модель идемпотентности справляется с дублированием доставки. Работа лежит на стороне подписчика - создание надежного обработчика - и эта работа невелика по сравнению с созданием конвейера приема на основе опроса.
Операционные инструменты
Страница вебхуков в дашборде показывает три вещи для каждой подписки:
- История доставки: каждое отправленное событие, HTTP-статус, возвращенный подписчиком, задержка и метка времени следующей попытки (если есть).
- Повтор (Replay): кнопка для повторной отправки каждого события. Полезно для тестирования изменений вашего обработчика.
- Тестовая конечная точка: кнопка для каждой подписки для отправки синтетического тестового события без имитации реального клика. Тестовое событие имеет
type: "test"и фиксированную полезную нагрузку.
Конечные точки повтора и теста также доступны как API-эндпоинты (POST /v1/webhooks/{id}/events/{evt_id}/replay и POST /v1/webhooks/{id}/test).
Для отладки высокой пропускной способности руководство по наблюдаемости описывает, как подключить доставку вебхуков к вашим собственным метрикам - каждая отправка экспортируется как счетчик Prometheus и гистограмма.
Внешние ссылки
- Шпаргалка OWASP по безопасности вебхуков - обоснование схемы подписи.
- Документация Stripe по вебхукам - эталонная реализация вебхуков с подписью HMAC.
- RFC 7234 - Кэширование в HTTP/1.1 - семантика
Retry-After.
Читайте также
- Смарт-ссылки: объяснение - краеугольный камень кластера функций.
- Вебхуки против опроса для отслеживания кликов - когда что выбрать.
- Быстрый старт по API + SDK сокращателя ссылок - входящая часть API.
- Fire-and-forget прием кликов - очередь за диспетчером.
- Серверная отслеживаемость конверсий - то, что вызывает событие
conversion. - Поверхности продукта:
/features/webhooks,/solutions/developers. - Руководства по эксплуатации:
/docs/guides/conversion-forwarding,/docs/guides/observability.
Попробуйте Elido
Вставьте URL - получите короткую ссылку
Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.
Бесплатно, без регистрации · 2 в день