8 мин чтенияИнтеграции

Вебхуки или опрос: вебхуки кликов в сравнении

Вебхуки или опрос для отслеживания кликов: задержка и число запросов, повторы, идемпотентность и подписи, таблица выбора и код на Node.

Sasha Ehrlich
Compliance · EU residency

Используйте вебхуки, когда человек или рабочий процесс должен отреагировать за секунды, а опрашивайте, когда несколько минут задержки допустимы и вы предпочитаете не держать публичный эндпоинт. В Elido у этого выбора есть подвох: вебхуков для отслеживания кликов пока не существует. Вебхуки покрывают события ссылок, доменов, участников и рабочего пространства, а событие click.created по каждому клику запланировано, но не отправляется. Сегодня данные о кликах получают опросом API аналитики.

В этой статье мы считаем задержку и объём запросов, разбираем, что гарантирует каждый паттерн (повторы, дубликаты, порядок, подписи), и завершаем таблицей выбора. Код был запущен на Node 22 против локальных моков, прежде чем попасть сюда. Полный каталог событий ссылок смотрите в статье вебхуки для событий ссылок; про эндпоинты кликов - в руководстве по API аналитики ссылок.

Вебхуки и опрос: по абзацу на каждый

Опрос означает, что ваш код по расписанию запрашивает новые данные. Cron-задача вызывает GET /v1/workspaces/{workspace_id}/analytics/clicks/recent, обрабатывает новое и засыпает. Каждый запрос инициируете вы. Публичный URL не нужен.

Вебхуки меняют направление. Elido отправляет подписанный POST с JSON-конвертом на ваш HTTPS-эндпоинт, когда срабатывает событие, а ответ 2xx отмечает доставку выполненной. Нужен доступный эндпоинт. TLS. Обработчик, переживающий дубликаты. Сегодня события такие: link.created, link.updated, link.expired, domain.verified и остальные из списка в документации по вебхукам.

Схема бок о бок: при опросе ваша cron-задача отправляет GET в API Elido и забирает строки каждые пять минут; при вебхуках сервер Elido отправляет POST на ваш HTTPS-эндпоинт, который возвращает 2xx, а событие click.created по каждому клику всё ещё запланировано. Страница 'Вебхуки' в Elido со списком эндпоинта с подписанными событиями ссылок, активным статусом и датой создания

Арифметика задержки и стоимости

Задержка опроса определяется интервалом. При интервале I минут событие ждёт в среднем I/2 минуты и в худшем случае I минут, плюс та задержка, с которой данные становятся доступны для запроса. Запросов в сутки 1440 / I.

  • 1 минута: 1440 запросов в сутки, 30 секунд средней задержки, 1 минута в худшем случае.
  • 5 минут: 288 запросов, в среднем 2,5 минуты, в худшем случае 5 минут.
  • 15 минут: 96 запросов, в среднем 7,5 минуты, в худшем случае 15 минут.
  • 60 минут: 24 запроса, в среднем 30 минут, в худшем случае 60 минут.

Стоимость проявляется в напрасных запросах, а не в давлении лимитов. Опубликованный лимит рабочего пространства на Free - 10 запросов в секунду, то есть 864 000 в сутки (см. лимиты запросов API и идемпотентность). Один опросчик с интервалом в 1 минуту использует 1440 / 864 000 = 0,17% этого. Лимитов вы достигнете, когда опрашиваете каждую ссылку отдельно среди сотен ссылок или когда рабочее пространство делит другая автоматизация, а не с одной лентой.

Важно именно число напрасных запросов. Допустим, рабочее пространство меняет 40 ссылок в сутки, а вы следите за этим опросом раз в минуту. Не более 40 из 1440 запросов вернут что-то новое, то есть минимум 1400 (97%) пустые. Получатель вебхуков получит 40 запросов на те же 40 событий. В худшем случае, с тремя попытками на каждое, это 120.

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

Гарантии доставки: что обещает Elido

Эти поведения взяты из кода доставки и справочной документации, проверено 1 октября 2026.

Повторы. Доставка получает три попытки: одну сразу, одну через 5 минут после сбоя, одну через 15 минут после этого, всего около 20 минут от начала до конца. Любой статус не 2xx, сетевая ошибка или ответ медленнее 10 секунд считаются сбоем. После третьей доставка помечается неудавшейся; Retry в панели или POST .../deliveries/{delivery_id}/retry снова взводит её со свежим бюджетом из трёх попыток. Для сравнения: Stripe повторяет события live-режима до трёх суток. Двадцать минут - это мало. Не рассчитывайте, что простой получателя будет покрыт.

Идемпотентность. Доставка идёт как минимум один раз. Заголовок X-Webhook-Delivery идентифицирует одну доставку на один эндпоинт и не меняется при автоматических или ручных повторах, так что это ваш ключ дедупликации.

Порядок. Обещания порядка нет. Повтор раннего события может прийти после более позднего, поэтому сравнивайте data.updated_at с сохранённым, прежде чем что-либо перезаписывать.

Подписи. Каждая подписанная доставка несёт X-Webhook-Signature (псевдоним X-Elido-Signature) со значением v1= плюс hex HMAC-SHA256 от {timestamp}.{raw_body}, а метку времени - в X-Webhook-Timestamp. Метка времени свежая при каждой попытке, поэтому законный повтор никогда не бывает устаревшим. Во время ротации секрета X-Elido-Signature-Previous несёт подпись от старого секрета в течение семи дней. Статья проверка подписей вебхуков охватывает Python, Go и отладку несовпадения.

Транспорт и пауза. Новые эндпоинты событий должны использовать https; обычный http принимается только для localhost и 127.0.0.1. Установка is_active в false ставит эндпоинт на паузу. После исправления от 22 сентября это останавливает и доставки в очереди.

Получатель вебхуков на Node

Этот получатель проверяет подпись по сырым байтам, задаёт окно в пять минут, принимает заголовок предыдущего секрета во время ротации и дедуплицирует по ID доставки. Допуск в пять минут - ваш выбор, а не Elido, и он совпадает с умолчанием Stripe. Я запустил его против локального подписчика: валидный запрос вернул 200, повтор с тем же ID доставки вернул "duplicate", неверный секрет и метка времени 400-секундной давности вернули 401.

import http from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.WEBHOOK_SECRET; // the whsec_... value
const seen = new Set(); // use a unique index in a real database

export function verify(secret, headers, rawBody, toleranceSec = 300) {
  const ts = headers["x-webhook-timestamp"] ?? "";
  if (!/^\d+$/.test(ts)) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false;
  const mac = createHmac("sha256", secret).update(`${ts}.`).update(rawBody);
  const expected = Buffer.from("v1=" + mac.digest("hex"));
  return ["x-elido-signature", "x-elido-signature-previous"].some((name) => {
    const got = Buffer.from(headers[name] ?? "");
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}

http
  .createServer((req, res) => {
    const chunks = [];
    req.on("data", (c) => chunks.push(c));
    req.on("end", () => {
      const raw = Buffer.concat(chunks);
      if (req.method !== "POST" || !verify(SECRET, req.headers, raw)) {
        return res.writeHead(401).end();
      }
      const id = req.headers["x-webhook-delivery"];
      if (seen.has(id)) return res.writeHead(200).end("duplicate");
      seen.add(id);
      const event = JSON.parse(raw.toString("utf8"));
      queueMicrotask(() => console.log("handled", event.type)); // do the work after replying
      res.writeHead(200).end("ok");
    });
  })
  .listen(8787);

Проверка длины перед timingSafeEqual важна. Node выбрасывает исключение на буферах разного размера. Сначала ответьте, потом работайте. Отсечка в 10 секунд - то место, где медленный, но успешный обработчик превращается в дубликат.

Опросчик кликов на Node

Пока не выйдет click.created, это и есть путь для кликов. clicks/recent возвращает сначала новейшие, принимает limit до 500 (по умолчанию 100) и возвращает next_cursor, когда страница пришла полной. Каждый запуск читает с самого верха и останавливается, дойдя до новейшего клика, который уже обработан. Я запустил его против мока, который отдаёт по две строки на страницу: первый запуск обработал 5 кликов, второй только 2 новых, третий ни одного.

const BASE = "https://api.elido.app";
const WS = process.env.ELIDO_WORKSPACE_ID;
const KEY = process.env.ELIDO_API_KEY;

// Newest click already handled; clicks/recent is newest first.
let lastSeen = null;
const keyOf = (c) => `${c.ts}|${c.link_id}`;

export async function pollOnce(handle) {
  let cursor = null;
  let newest = null;
  for (;;) {
    const url = new URL(`${BASE}/v1/workspaces/${WS}/analytics/clicks/recent`);
    url.searchParams.set("limit", "500");
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${KEY}` },
      signal: AbortSignal.timeout(10_000),
    });
    if (!res.ok) throw new Error(`clicks/recent returned ${res.status}`);
    const { items, next_cursor } = await res.json();
    for (const click of items) {
      if (keyOf(click) === lastSeen) {
        if (newest) lastSeen = newest;
        return;
      }
      newest ??= keyOf(click);
      await handle(click);
    }
    if (!next_cursor) break;
    cursor = next_cursor;
  }
  if (newest) lastSeen = newest;
}

// setInterval(() => pollOnce(handleClick).catch(console.error), 60_000);

Два ограничения, сказанные прямо. Курсор упорядочивает по метке времени и ID ссылки, поэтому два клика по одной ссылке в одну и ту же секунду делят ключ; если вы не можете позволить себе редкий пропущенный или повторный клик, сделайте handle идемпотентным по полной строке. И lastSeen живёт в памяти, поэтому сохраняйте его между перезапусками, иначе перезапуск воспроизведёт недавнюю историю.

Не готовы строить ни то, ни другое? Это нормально. Откройте панель управления и сначала пользуйтесь аналитикой в ней, а API добавьте, когда поймёте, какие числа вам нужны по расписанию.

Таблица выбора

СитуацияЧто использовать сегодняПочему
Реагировать на изменения ссылок или доменов за секундыВебхук на link.*, domain.*События приходят пушем; событие клика не нужно
Живой счётчик кликов в вашем продуктеОпрос clicks/recent раз в минутуВебхука кликов пока нет; 1440 запросов в сутки - далеко в пределах лимита
Обогащение CRM по каждому кликуПересыльщик событий или частый опросСредняя задержка опроса в 30 секунд обычно приемлема
Еженедельная или ежемесячная отчётностьОпрос summary и timeseriesРетроспективным данным пуш ничего не даёт
Нет доступного публичного эндпоинтаОпросВебхукам нужен https-адрес, доступный из интернета
Нельзя потерять событие ссылкиВебхук плюс ночная сверкаПовторы покрывают около 20 минут, остальное покрывает опрос

Когда выйдет поток click.created с выборкой, строки про живой счётчик и CRM переедут в столбец вебхуков, а опрос станет проходом сверки. До тех пор пересыльщики событий вроде событий кликов по ссылкам в Mixpanel - вариант в стиле пуша для данных о кликах.

Гибридный паттерн

Для всего, что нельзя потерять, запускайте оба. Вебхук - быстрый путь. Плановый опрос сравнивает итоги и находит пробелы: доставки, исчерпавшие три попытки во время простоя, или события, пришедшие не по порядку.

Отчёты summary и timeseries возвращают итоги и суточные корзины за диапазон дат одним вызовом, что делает ночное сравнение дешёвым. Помните, что фильтр ботов убирает краулеры и мониторы до записи кликов, поэтому ваши итоги не совпадут с сырыми логами сервера; что исключается, объясняет руководство по аналитике. Остальное охватывает чек-лист безопасности. Если вы проектируете собственные вебхуки, полезный справочник - спецификация Standard Webhooks.

Страница документации API в панели управления Elido со ссылками на справочник API, SDK, спецификацию OpenAPI и управление API-ключами

Экран документации API в DEMO-рабочем пространстве с примерными данными; это не аккаунт клиента.

Чек-лист перед запуском для любого паттерна

Прежде чем выпускать получателя или опросчика, пройдите шесть проверок. Большинство инцидентов в продакшене с обоими паттернами возникает из-за пропуска одной из них.

  • Вебхук: отвечайте 2xx в течение 10 секунд, а медленную работу делайте после ответа.
  • Вебхук: проверяйте подпись по сырому телу до разбора JSON.
  • Вебхук: сохраняйте X-Webhook-Delivery с уникальным индексом, чтобы повтор не мог посчитаться дважды.
  • Опросчик: сохраняйте курсор или ключ последнего увиденного вне памяти процесса.
  • Опросчик: при 429 или 5xx замедляйтесь, а не повторяйте с обычным интервалом.
  • Оба: поднимайте тревогу, когда ни событие, ни новый клик не приходили дольше вашего обычного тихого периода, потому что именно тишиной выглядит сломанная интеграция. Независимые материалы, например руководство Hookdeck по вебхукам и опросу API, приходят к тому же совету: пуш ради скорости, опрос ради сверки.

Читайте также в блоге

Частые вопросы

В чём разница между вебхуками и опросом?

При опросе ваш код по расписанию запрашивает у API новые данные. При вебхуках сервер отправляет HTTP POST на ваш эндпоинт, когда что-то происходит. Опрос стоит запросов и добавляет задержку до одного интервала. Вебхуки требуют всегда доступного публичного эндпоинта и работы с повторами и дубликатами.

Есть ли в Elido вебхуки для отслеживания кликов?

Пока нет. Вебхуки покрывают события ссылок, доменов, участников и рабочего пространства, такие как link.created и link.expired. Событие click.created запланировано, но сейчас его никто не публикует. Для данных на уровне клика опрашивайте отчёт clicks/recent API аналитики с его курсором или используйте пересыльщик событий.

Как часто опрашивать API аналитики?

Подбирайте интервал под решение, которое питают данные. Одна минута подходит для почти живого счётчика, от пяти до пятнадцати минут для дашбордов, раз в час или раз в сутки для отчётов. Опрос раз в минуту даёт 1440 запросов в сутки, а раз в 5 минут - 288, так что чаще редко имеет смысл.

Гарантируется ли доставка вебхука ровно один раз?

Нет. Доставка вебхука идёт по принципу «как минимум один раз», поэтому получатель видит дубликаты после таймаута или ручной повторной отправки. Elido сохраняет одно и то же значение X-Webhook-Delivery при повторах, поэтому сохраняйте этот ID и пропускайте всё, что уже обработали.

Что будет, если мой эндпоинт вебхука недоступен?

Elido делает три попытки: одну сразу, одну через пять минут после сбоя и одну через пятнадцать минут после этого. Если все три неудачны, доставка помечается как неудавшаяся, пока кто-то не нажмёт Retry или не вызовет эндпоинт повтора. Простои дольше примерно двадцати минут требуют задачи сверки.

Можно ли сочетать вебхуки и опрос?

Да, и для всего важного нужно. Используйте вебхук как быстрый путь, а плановый опрос как страховочную сетку, которая находит события, пропущенные вашим получателем. Опрос можно запускать раз в час или раз в ночь, потому что ему нужно только закрывать пробелы.

Попробуйте Elido

Вставьте URL - получите короткую ссылку

Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.

Без регистрации. Зарегистрируйтесь бесплатно и получите 50 ссылок в месяц.

Попробуйте Elido

URL-сокращатель с хостингом в ЕС: собственные домены, глубокая аналитика, открытый API. Бесплатный тариф - без банковской карты.

Теги
webhooks vs polling
click tracking webhooks
click tracking api
url shortener webhooks
webhook retries idempotency
link analytics api

Читать дальше