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

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

Вебхуки URL-скорочувача для подій посилань: реальна структура payload, перевірки HMAC X-Webhook-Signature на Node і Python, політика повторних спроб і ключі дедублікації.

Marius Voß
DevRel · edge infra
Діаграма вебхуків URL-скорочувача у піксель-стилі: події link.created, link.updated і member.invited проходять через підписану доставку на кінцеві точки event, siem і discord, під смугою з текстом HMAC-SHA256 v1=, таймаут 10с, 3 спроби

Вебхуки URL-скорочувача Elido надсилають POST з підписаним JSON-конвертом на вашу кінцеву точку HTTPS щоразу, коли щось змінюється в робочому просторі: посилання створено, відредаговано, видалено, минув термін дії чи досягнуто ліміт кліків, запрошено учасника, верифіковано домен. Кожен запит несе заголовок X-Webhook-Signature: v1=<hex>, що є HMAC-SHA256 над {timestamp}.{raw_body}, а невдала доставка отримує три спроби приблизно за двадцять хвилин.

Чого вона не надсилає сьогодні - це webhook кліку по посиланню. Кліки натомість виходять через API аналітики і пересилачі подій, і я покажу де саме наприкінці. Цей матеріал - вихідна половина поверхні API; швидкий старт з API + SDK URL-скорочувача розглядає вхідну половину, а пояснення розумних посилань - наріжний матеріал кластера функцій, звідки походять події посилань.

Які події посилань сьогодні запускають webhook

Кожна подія нижче доходить до кінцевої точки webhook, яка підписалася на неї за назвою. Форма нової кінцевої точки в дашборді пропонує чекбокси для восьми найпоширеніших; API приймає будь-яку назву зі списку.

ПодіяСпрацьовує, колиЧекбокс у дашборді
link.createdПосилання створено, поодинці чи під час масового імпортутак
link.updatedЗмінюється ціль, налаштування чи статус, масові редагування, відновленнятак
link.deletedПосилання видалено, поодинці чи масовотак
link.expiredУ посилання минає термін діїлише API
link.cap_reachedПосилання досягає максимальної кількості кліківлише API
link.brokenПеревірка непрацюючих посилань бачить, що ціль не відповідаєлише API
workspace.created, workspace.updatedРобочий простір створено або змінено його налаштуваннятак
member.invited, member.removedУчасника додано (напряму, через SCIM чи прийняте запрошення) або видаленотак
member.role_changedРоль учасника зміниласялише API
invitation.created, invitation.acceptedЗапрошення надіслано чи прийнятолише API
domain.verified, domain.ssl_failedВласний домен проходить перевірки DNS, або досі не проходить їх через 24 годинилише API
audit.eventБудь-який запис журналу аудитутак

Зашифровані посилання додають ще дві: link.encrypted_created і link.encryption_rotated. Якщо ви не хочете вручну підтримувати список актуальним, створіть кінцеву точку siem: вона отримує кожну подію в робочому просторі, включно із записами аудиту, взагалі без фільтра підписки.

У дорожній карті, але ще не в роботі: click.created (семплований потік кліків), події білінгу на кшталт billing.subscription_upgraded і фільтри для окремої кінцевої точки на кшталт «лише посилання в теці X». Не підписуйтеся на ці назви сьогодні; їх ніхто не публікує.

Створення кінцевої точки webhook через API

Кінцева точка належить одному робочому простору. Ви створюєте її POST-запитом на /v1/workspaces/{workspace_id}/webhooks:

curl -X POST "https://api.elido.app/v1/workspaces/$WORKSPACE_ID/webhooks" \
  -H "Authorization: Bearer elido_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/elido",
    "events": ["link.created", "link.updated", "link.deleted"],
    "description": "CRM sync",
    "kind": "event"
  }'

Відповідь - 201 з кінцевою точкою і, для типів event і siem, одноразовим секретом:

{
  "endpoint": {
    "id": 7,
    "workspace_id": 42,
    "url": "https://hooks.example.com/elido",
    "events": ["link.created", "link.updated", "link.deleted"],
    "is_active": true,
    "description": "CRM sync",
    "kind": "event",
    "config": {},
    "created_at": "2026-09-21T09:12:44Z"
  },
  "secret": "whsec_9f2c..."
}

Elido генерує секрет; ви його не надсилаєте. Скопіюйте його зараз, бо жоден пізніший виклик його не поверне. kind приймає п'ять значень. event і siem - це підписані JSON-доставки, які розглядає цей матеріал. discord, telegram і sentry переформатовують ті самі події у повідомлення чату чи подію Sentry, автентифікуються через URL-адресу чи зашифрований токен бота, і не несуть заголовків HMAC.

Дозволи змінилися цього місяця. Читання кінцевих точок і журналу доставок потребує workspace.view. Створення, редагування, видалення, ротація секрету чи повторне надсилання доставки потребує workspace.edit, тобто ролі адміністратора чи власника. API-ключ працює всередині робочого простору, для якого його видали, і ніколи не вище ролі, обраної під час створення, тож ключ рівня viewer отримає 403 на POST вище. Повний довідник: документація з вебхуків.

Конверт payload webhook

Кожна підписана доставка має той самий конверт з чотирьох полів. data містить запис, що змінився, тож для подій посилань це рядок посилання:

{
  "type": "link.created",
  "workspace_id": 42,
  "data": {
    "id": 91834,
    "workspace_id": 42,
    "domain_id": 3,
    "slug": "spring-sale",
    "destination_url": "https://shop.example.com/spring",
    "title": "Spring sale landing",
    "tags": ["newsletter"],
    "status": "active",
    "expires_at": null,
    "max_clicks": null,
    "redirect_status": 302,
    "created_by_user_id": 17,
    "created_at": "2026-09-21T09:14:02.184311Z"
  },
  "timestamp": "2026-09-21T09:14:02Z"
}

Цей приклад скорочено; реальний data несе кожен стовпець посилання, включно з правилами таргетингу, текою, кампанією і полями сканування. У тілі немає ні ID події, ні short_url, тож будуйте коротку URL-адресу з вашого домену і slug, якщо вона вам потрібна. Заплановані події надсилають менший об'єкт: link.expired має link_id, slug і destination_url, а link.cap_reached додає cap і clicks.

Одне виправлення, про яке варто знати, якщо ви логували payload до цього тижня: секретні поля тепер видаляються з payload перед тим, як він покидає Elido. password_hash посилання, захищеного паролем, і token запрошення раніше з'являлися в data; тепер вони не з'являються ні в нових доставках, ні в журналі доставок. Якщо ви зберігали старі payload, ці дані варто видалити.

Перевірка заголовка X-Webhook-Signature

Кожен підписаний запит несе ці заголовки:

X-Webhook-Signature: v1=5d8f0c3e...
X-Elido-Signature: v1=5d8f0c3e...
X-Webhook-Timestamp: 1790068442
X-Webhook-Event: link.created
X-Webhook-Delivery: 55120
User-Agent: Elido-Webhooks/1.0

Обидва заголовки підпису містять однакове значення. Elido обчислює HMAC-SHA256, як визначено в RFC 2104, з усім рядком whsec_... як ключем, над міткою часу, крапкою і сирими байтами тіла. Hex-дайджест отримує префікс v1=. Усередині заголовка немає поля t=; мітка часу живе у власному заголовку.

Потік перевірки підпису: заголовки X-Webhook-Signature і X-Webhook-Timestamp плюс сире тіло і секрет живлять HMAC-SHA256 над ts крапка body, порівнюваний за постійний час як v1= hex, потім 5-хвилинний фільтр свіжості на боці отримувача розгалужується на обробку події або відхилення з 400

У Node підписуйте сирі байти, а не повторно серіалізований об'єкт. Express потребує express.raw({ type: "application/json" }) на цьому маршруті саме тому:

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

export function verifyElido(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"));

  // During a rotation the old secret signs X-Elido-Signature-Previous.
  return ["x-elido-signature", "x-elido-signature-previous"].some((name) => {
    const got = Buffer.from(headers[name] ?? "");
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}

Перевірка довжини важлива: timingSafeEqual Node кидає виняток на буферах різного розміру замість повернення false. Версія на Python зі стандартним модулем hmac:

import hashlib
import hmac
import time


def verify_elido(secret: str, headers, raw_body: bytes, tolerance: int = 300) -> bool:
    ts = headers.get("X-Webhook-Timestamp", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
        return False
    digest = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256)
    expected = "v1=" + digest.hexdigest()
    for name in ("X-Elido-Signature", "X-Elido-Signature-Previous"):
        got = headers.get(name)
        if got and hmac.compare_digest(got, expected):
            return True
    return False

Якщо перевірка продовжує не проходити, посібник із перевірки підписів вебхуків містить версію для Go, вузол Code в n8n та типові причини невідповідності. П'ятихвилинне вікно - це ваша перевірка, не наша. Elido проставляє свіжу мітку часу на кожній спробі, включно з повторами, тож легітимний повтор ніколи не виглядає застарілим. Без цього вікна будь-хто, хто перехопив один запит, міг би відтворити його наступного тижня, і підпис все одно збігся б.

Ротація - це POST /v1/workspaces/{workspace_id}/webhooks/{id}/rotate-secret, або кнопка Rotate на сторінці кінцевої точки. Ви отримуєте новий секрет один раз. Наступні сім днів кожна доставка також несе X-Elido-Signature-Previous, підписаний старим секретом, тому обидві функції вище пробують і його. Розгорніть новий секрет у будь-який момент цього тижня, і нічого не зламається.

Політика повторних спроб webhook

Воркер доставки підхоплює доставки, що очікують, кожні кілька секунд, тож зміна посилання зазвичай доходить до вас за секунди. Будь-яка відповідь 2xx позначає доставку виконаною. Статус, відмінний від 2xx, мережева помилка чи відсутність відповіді протягом 10 секунд рахується як невдала спроба.

Стовпчикова діаграма розкладу повторних спроб webhook: спроба 1 в T+0, спроба 2 через п'ять хвилин після збою в T+5хв, спроба 3 ще через п'ятнадцять хвилин в T+20хв, після чого доставка позначається невдалою без подальших автоматичних спроб, доки хтось не натисне Retry чи не викличе кінцеву точку retry

Три спроби на доставку, двадцять хвилин загалом. Це навмисно коротко, і чесно кажучи, коротше, ніж я б обрав для отримувача за нестабільним VPN. Двогодинний простій на вашому боці не покриють автоматичні повтори. Що його покриває - журнал доставок: GET /v1/workspaces/{workspace_id}/webhooks/{id}/deliveries перелічує кожну доставку зі статусом, HTTP-кодом, затримкою, кількістю спроб і часом наступного повтору, а сторінка кінцевої точки показує ті самі рядки з кнопкою Retry. Retry, або POST .../deliveries/{delivery_id}/retry, перезапускає невдалу чи доставлену доставку зі свіжим бюджетом у три спроби і повертає 202. Доставка, що ще очікує, отримує 409.

Деякі збої пропускають повторні спроби. Кінцева точка Telegram без chat_id, чи некоректний DSN Sentry, позначаються невдалими одразу, бо повторення запиту не виправить конфігурацію. Щоб вимкнути кінцеву точку, не видаляючи її, надішліть PUT з "is_active": false; призупинені кінцеві точки не отримують нових доставок.

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

Плануєте отримувача для вашої команди? Сторінка функції вебхуків показує сторону дашборду всього цього.

Ідемпотентність і порядок webhook

Elido доставляє за принципом at-least-once. Розділ про повторні спроби показав один спосіб виникнення дубліката: ваш обробник комітить роботу, потім не встигає в десятисекундне вікно, і Elido надсилає її знову. Ручний Retry надсилає повторно навмисно.

В обох випадках зберігається те саме значення X-Webhook-Delivery, бо це ID однієї доставки на одну кінцеву точку, а не однієї спроби. Ключуйте на ньому:

CREATE TABLE elido_webhook_seen (
  delivery_id BIGINT PRIMARY KEY,
  received_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- in the handler, inside the same transaction as your work:
INSERT INTO elido_webhook_seen (delivery_id) VALUES ($1)
ON CONFLICT (delivery_id) DO NOTHING
RETURNING delivery_id;
-- no row back means you've already handled this delivery

Дві кінцеві точки, підписані на ту саму подію, отримують два різні ID доставки, тож дедублікуйте для кожної кінцевої точки окремо. У рідкісних перезапусках на нашому боці подія може бути поставлена в чергу двічі як окремі доставки; якщо подвійний запис зашкодить, додайте другий захист на type плюс data.id плюс data.updated_at.

Гарантії порядку немає. Доставки виходять у порядку від найстарішої простроченої, але повтор ранньої події може прийти після пізнішої. Порівнюйте data.updated_at з тим, що ви вже зберегли, перш ніж перезаписувати посилання, і не покладайтеся на timestamp конверта для впорядкування: він має точність лише до секунди.

Де живуть дані кліків замість webhook кліків

Це та частина, де стара версія цього матеріалу помилялася. Сьогодні немає webhook click, і click.created запланований, а не реалізований. Шлях редиректу навмисно лишається вільним від синхронної роботи, що пояснює матеріал прийом кліків за принципом fire-and-forget, а кліки йдуть у сховище аналітики, а не в чергу webhook.

Для даних на рівні кліків зараз у вас є два шляхи:

  1. Пересилачі подій. Кожен клік по короткому посиланню стає подією на боці сервера в інструменті, який ви вже використовуєте: клікові події Mixpanel, клікові події Klaviyo в профілях чи метрики редиректу Datadog для операційних дашбордів.
  2. API аналітики. GET /v1/analytics/workspaces/{workspace_id}/clicks/recent повертає останні кліки, а clicks.csv експортує їх, тож заплановане завдання може забирати потрібні рядки.

Який варіант підходить, залежить від затримки і того, куди потрапляють дані; вебхуки проти опитування для відстеження кліків розглядає цей компроміс. І якщо семплований потік click.created вийде в реліз, ця сторінка повідомить про це першою.

Прочитайте наріжний матеріал: пояснення розумних посилань.

Пов'язане в блозі

Поширені запитання

Чи надсилає Elido webhook на кожен клік по посиланню?

Сьогодні - ні. Вебхуки покривають зміни в робочому просторі, такі як link.created, link.updated, link.expired і member.invited. Подія click.created - в дорожній карті як семплований потік. Для даних на рівні кліків зараз використовуйте API аналітики або пересилач подій на кшталт Mixpanel, Klaviyo чи Datadog.

Як перевірити підпис webhook Elido?

Обчисліть HMAC-SHA256 над значенням X-Webhook-Timestamp, крапкою і сирим тілом запиту, з ключем - вашим секретом whsec_. Закодуйте в hex, додайте префікс v1= і порівняйте за постійний час із заголовком X-Webhook-Signature. Відхиляйте мітки часу, старіші за п'ять хвилин.

Скільки разів Elido повторює невдалий webhook?

Кожна доставка отримує три спроби: одну одразу, одну через п'ять хвилин після збою і ще одну через п'ятнадцять хвилин після цього. Будь-який статус, відмінний від 2xx, мережева помилка чи відповідь повільніша за десять секунд рахується як збій. Після третьої спроби доставка позначається невдалою, доки ви не натиснете Retry.

У чому різниця між X-Webhook-Signature і X-Elido-Signature?

Нічого, крім назви. Обидва заголовки несуть той самий підпис v1=, і X-Webhook-Signature лишається для старіших отримувачів. Під час ротації секрету Elido також надсилає X-Elido-Signature-Previous, підписаний старим секретом, протягом семи днів.

Як зупинити повторну обробку того самого webhook?

Збережіть значення заголовка X-Webhook-Delivery і пропускайте будь-який запит, значення якого ви вже обробили. Він ідентифікує одну доставку на одну кінцеву точку і лишається незмінним при автоматичних повторах і ручних повторних надсиланнях, тож унікального індексу на ньому достатньо.

Хто може створювати чи видаляти вебхуки в робочому просторі?

Адміністратори і власники робочого простору. Перелік кінцевих точок і читання журналу доставок потребує доступу на перегляд; створення, редагування, видалення, ротація секрету чи повторне надсилання доставки потребує права workspace.edit. API-ключі обмежені роллю, обраною під час створення ключа.

Спробуйте Elido

Вставте URL - отримайте коротке посилання

Без реєстрації. Посилання живе 30 днів. Зареєструйтесь, щоб зберегти назавжди.

Безкоштовно, без реєстрації · 2 на день

Спробуйте Elido

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

Теги
url shortener webhooks
link click webhook
webhook signature verification
webhook retry policy
webhook idempotency
webhook payload

Читати далі