Elido
8 хв читанняМожливості

Вебхуки для подій посилань: будь-яка форма, будь-яка спроба повтору

Повний інтерфейс вебхуків для подій скорочувача URL — формати корисного навантаження для кліків, конверсій, link.created та bio.click, а також політика повторів, схема підпису та модель ідемпотентності

Marius Voß
DevRel · edge infra
Діаграма типу «зірка» з джерелами подій посилань зліва (кліки, конверсії, link.created, bio.click), які спрямовуються до центральної служби webhook-dispatcher, що розсилає їх на кінцеві точки підписників з анотаціями повторів: 1с, 30с, 5хв, 1год, 6год

Вебхуки — це та частина API скорочувача URL, яку реалізують усі, але майже ніхто не робить це добре. Складність полягає не в кодуванні (корисне навантаження — це JSON-об'єкт), а в операційних деталях: верифікації підпису, політиці повторів, ідемпотентності, гарантіях доставки та тому, що відбувається, коли кінцева точка підписника недоступна протягом двох днів.

Ця стаття документує кожну подію вебхуку, яку випускає Elido, кожен формат корисного навантаження, графік повторів та схему підпису. URL shortener API + SDKs quickstart охоплює вхідну поверхню 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 for URL shorteners пояснює чому; на практиці це означає, що ваш підписник отримує достатньо географічної точності для аналітики без відповідальності за обробку персональних даних у вигляді повних IP-адрес.
  • city_geoname_id, а не city_name. ID GeoNames є стабільним незалежно від локалі; назви міст варіюються. Якщо вам потрібна локалізована назва, зверніться до дампу GeoNames.org один раз і кешуйте результат.
  • user_agent_family, а не повний рядок UA. Ми видаляємо повний UA під час прийому (це дані високої ентропії для цифрових відбитків); сімейство — це браузер + основна версія, яка залишається.

Затримка між обслуговуванням перенаправлення клієнту та спрацюванням вебхуку зазвичай становить від 200 мс до 2 с. Події кліків спочатку проходять через Redpanda, агрегуються для аналітики, а потім працівник (worker) розсилки генерує вебхуки. Це той самий конвеєр, що забезпечує аналітику на панелі приладів — стаття fire-and-forget click ingestion охоплює механіку черги.

Формат події конверсії (conversion)#

Події конверсії спрацьовують, коли клік співставляється з подальшою конверсією — покупкою, реєстрацією, заповненням форми або будь-якою іншою дією, яку ви підключаєте до conversion forwarding pipeline.

{
  "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.50,
    "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 cheat sheet on webhook security; ми не винайшли цей шаблон, ми просто його реалізували.

Політика повторів#

Це частина, де більшість реалізацій вебхуків стають недбалими.

Вебхук спрацьовує один раз за щасливим сценарієм: підписник повертає 2xx, диспетчер фіксує успіх, подія завершена. Складніші випадки — це відповіді не 2xx, мережеві помилки та підписники, які повільно відповідають.

Графік повторів Elido:

СпробаЗатримка після попередньоїКумулятивноСтатус
10початковий
2перший повтор
330с31с
45хв5хв 31с
51год1год 5хв 31с
66год7год 5хв 31с
724год31год 5хв 31сфінальний

Після 7-ї спроби (~31 годину після першої) диспетчер здається і випускає внутрішню подію webhook.failed. Кінцева точка підписника позначається як деградована після трьох послідовних збоїв у будь-яких подіях; деградовані підписки отримують зменшений бюджет повторів на 24 години. Після 50 послідовних збоїв підписка призупиняється, а власник робочого простору отримує сповіщення.

Поведінка повторів поважає заголовки Retry-After від підписника. Якщо ваша кінцева точка обмежує швидкість Elido (повертаючи 429 з Retry-After: 120), наступна спроба чекатиме 120 секунд замість стандартних 30 секунд за графіком.

Неможливість відповісти протягом 10 секунд розцінюється як тайм-аут і зараховується як невдала спроба. Бюджет у 10 секунд надається спеціально — він покриває затримку холодного старту (cold-start) у безсерверних (serverless) підписників. Але якщо ваша кінцева точка регулярно відповідає довше 5 секунд, виправте це в першу чергу; це коштуватиме вам обсягу повторів.

Ідемпотентність#

Підписники можуть отримувати одну і ту ж подію більше одного разу.

Це не помилка; це наслідок того, як працює розподілена доставка повідомлень. Якщо підписник повертає 504, тому що їхній бекенд був повільним, але врешті-решт обробив подію, диспетчер повторить спробу; підписник отримає її двічі і може обробити двічі. Та ж подія може виникнути двічі, якщо диспетчер аварійно завершується під час доставки, і подія повертається в чергу.

Мітігація (захист): кожна подія має унікальний id (префікс evt_…). Підписники повинні зберігати ідентифікатори, які вони вже обробили (маленька таблиця «ключ-значення» працює; TTL у 14 днів покриває вікно повторів із запасом) і пропускати події, ID яких вони вже бачили.

CREATE TABLE webhook_processed_events (
    event_id TEXT PRIMARY KEY,
    received_at TIMESTAMPTZ DEFAULT now()
);

-- у вашому обробнику:
INSERT INTO webhook_processed_events (event_id) VALUES ($1)
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
-- якщо RETURNING порожній, ви вже обробили цю подію

ON CONFLICT DO NOTHING — це дешева перевірка ідемпотентності. Якщо INSERT повертає рядок, це перший раз, коли ви бачите подію; якщо він нічого не повертає, ви вже її обробили.

Для підписників із високою пропускною здатністю (>1000 подій/сек) спеціальний Redis SETNX з TTL працює так само, але з меншими витратами, ніж рядок Postgres.

Впорядкування доставки#

Немає гарантії глобального впорядкування. Події з одного link_id надсилаються в порядку надходження, але події з різних посилань можуть надходити впереміш. Подія click у момент T+0 і подія conversion у момент T+10мс можуть прийти до вашого підписника в будь-якому порядку залежно від стану пулу працівників.

Часові мітки created_at є авторитетними для впорядкування. Якщо вашому підписнику потрібне суворе впорядкування, сортуйте за created_at на стороні сервера перед обробкою.

Для шляху клік → конверсія спеціально: подія конверсії завжди посилається на click_id події кліку, тому ви можете об'єднати їх на стороні сервера, навіть якщо вони приходять у неправильному порядку.

Вебхуки проти опитування — компроміс#

Стаття webhooks vs polling for click tracking охоплює це детально. Коротка відповідь: вебхуки є правильним шаблоном, коли (а) вам потрібна низька затримка при надходженні події (<5 секунд), і (б) ваш підписник доступний з публічного інтернету з TLS. Опитування (polling) є правильним шаблоном, коли (а) вам не потрібен реальний час, (б) ви контролюєте сховище даних і просто хочете щоденну/погодинну пакетну обробку, або (в) ваш підписник знаходиться в мережі, яка не приймає вхідний трафік.

Для більшості команд вебхуки — це відповідь. Графік повторів витончено обробляє тимчасові збої; схема підпису забезпечує безпеку; модель ідемпотентності обробляє дублювання доставки. Робота лежить на стороні підписника — створення надійного обробника — і ця робота невелика порівняно зі створенням конвеєра прийому даних на основі опитування.

Операційний інструментарій#

Сторінка вебхуків на панелі приладів показує три речі для кожної підписки:

  1. Історія доставки: кожна відправлена подія, HTTP-статус, який повернув підписник, затримка та час наступної спроби повтору (якщо є).
  2. Replay (відтворення): кнопка для кожної події, щоб відтворити її. Корисно для тестування змін у вашому обробнику.
  3. Тестова кінцева точка: кнопка для кожної підписки, щоб надіслати синтетичну тестову подію без реального кліку. Тестова подія має type: "test" і фіксоване корисне навантаження.

Кінцеві точки для відтворення та тестування також доступні як API-ендпоінти (POST /v1/webhooks/{id}/events/{evt_id}/replay та POST /v1/webhooks/{id}/test).

Для налагодження при високій пропускній здатності посібник з observability охоплює, як підключити доставку вебхуків до ваших власних метрик — кожна відправка експортується як Prometheus-лічильник та гістограма.

Зовнішні посилання#

Пов'язані матеріали#

Спробуйте Elido

URL-скорочувач із хостингом у ЄС: власні домени, глибока аналітика, відкритий API. Безкоштовний тариф — без кредитної картки.

Теги
вебхуки скорочувача URL
вебхук кліку по посиланню
повтор вебхуку
підпис вебхуку
ідемпотентність вебхуку
доставка подій
корисне навантаження вебхуку

Читати далі