Webhook URL-шортенера Elido отправляют подписанную JSON-обертку на ваш HTTPS endpoint при каждом изменении в рабочем пространстве: ссылка создана, отредактирована, удалена, истекла или достигла лимита кликов, приглашен участник, подтвержден домен. Каждый запрос несет заголовок X-Webhook-Signature: v1=<hex>, который представляет собой HMAC-SHA256 по {timestamp}.{raw_body}, а неудачная доставка получает три попытки примерно за двадцать минут.
Чего он пока не отправляет - так это webhook на клик по ссылке. Клики вместо этого уходят через API аналитики и пересыльщики событий, и в конце я покажу где именно. Этот пост - исходящая половина поверхности API; быстрый старт API + SDK URL-шортенера описывает входящую половину, а умные ссылки: как это работает - опорная статья кластера функций, из которой берутся события ссылок.
Какие события ссылок сегодня запускают webhook
Каждое событие ниже доходит до endpoint webhook, подписанного на него по имени. Форма создания нового endpoint в дашборде предлагает чекбоксы для восьми самых частых событий; 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. Если вы не хотите вручную поддерживать список актуальным, создайте endpoint типа siem: он получает каждое событие в рабочем пространстве, включая записи аудита, вообще без фильтра подписки.
В дорожной карте, но пока не в продакшене: click.created (выборочный поток кликов), события биллинга вроде billing.subscription_upgraded и фильтры по каждому endpoint вроде «только ссылки в папке X». Не подписывайтесь на эти имена сегодня; их ничего не публикует.
Создание endpoint webhook через API
Endpoint принадлежит одному рабочему пространству. Вы создаете его 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 с endpoint и, для типов 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.
В этом месяце изменились разрешения. Для чтения endpoint и журнала доставки нужен workspace.view. Для создания, редактирования, удаления, ротации секрета или повторной отправки доставки нужен workspace.edit, а это значит - права администратора или владельца. API-ключ работает внутри рабочего пространства, для которого он выпущен, и никогда не превышает роль, выбранную при его создании, так что ключ уровня viewer получит 403 на POST выше. Полный справочник - документация по webhook.
Обертка 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=; метка времени живет в собственном заголовке.
На 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
Если проверка по-прежнему не проходит, в руководстве по проверке подписей webhook есть версия на Go, узел Code в n8n и обычные причины несовпадения. Пятиминутное окно - это ваша проверка, не наша. Elido проставляет свежую метку времени на каждую попытку, включая повторы, так что легитимный повтор никогда не выглядит устаревшим. Без этого окна любой, кто перехватил один запрос, мог бы воспроизвести его на следующей неделе, и подпись все равно бы совпала.
Ротация выполняется через POST /v1/workspaces/{workspace_id}/webhooks/{id}/rotate-secret или кнопку Rotate на странице endpoint. Новый секрет вы получаете один раз. В течение следующих семи дней каждая доставка также несет X-Elido-Signature-Previous, подписанный старым секретом, поэтому обе функции выше пробуют и его. Разверните новый секрет в любой момент на этой неделе, и ничего не сломается.
Политика повторов webhook
Воркер доставки забирает ожидающие доставки каждые несколько секунд, так что изменение ссылки обычно доходит до вас за секунды. Любой ответ 2xx помечает доставку выполненной. Статус не 2xx, сетевая ошибка или отсутствие ответа в течение 10 секунд считается неудачной попыткой.
Три попытки на доставку, двадцать минут от начала до конца. Это намеренно коротко, и, честно говоря, короче, чем я бы выбрала для получателя за нестабильным VPN. Двухчасовой простой на вашей стороне не будет покрыт автоматическими повторами. Что его покрывает - журнал доставки: GET /v1/workspaces/{workspace_id}/webhooks/{id}/deliveries перечисляет каждую доставку со статусом, кодом HTTP, задержкой, числом попыток и временем следующего повтора, а страница endpoint показывает те же строки с кнопкой Retry. Retry, или POST .../deliveries/{delivery_id}/retry, заново запускает неудачную или доставленную доставку со свежим бюджетом в три попытки и возвращает 202. Доставка, все еще ожидающая, получает 409.
Некоторые сбои пропускают повторы. Endpoint Telegram без chat_id или некорректный DSN Sentry помечаются неудачными сразу, потому что повтор запроса не исправит настройку. Чтобы отключить endpoint, не удаляя его, отправьте PUT с "is_active": false; приостановленные endpoint не получают новых доставок.
Если ваш обработчик выполняет тяжелую работу, сначала верните 200 и поставьте задачу в очередь. Десятисекундный порог - это как раз то место, где медленный, но успешный обработчик превращается в дубликат, что подводит нас к дедупликации.
Планируете получатель для своей команды? Страница функции webhook показывает сторону дашборда для всего этого.
Идемпотентность и порядок webhook
Elido доставляет не менее одного раза. Раздел о повторах показал один способ возникновения дубликата: ваш обработчик фиксирует работу, затем не укладывается в десятисекундное окно, и Elido отправляет его снова. Ручной Retry отправляет повторно намеренно.
В обоих случаях значение X-Webhook-Delivery остается тем же самым, потому что это ID одной доставки на один endpoint, а не одной попытки. Используйте его как ключ:
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
Два endpoint, подписанных на одно и то же событие, получают два разных ID доставки, так что дедуплицируйте по каждому endpoint отдельно. При редких перезапусках на нашей стороне событие может быть поставлено в очередь дважды как отдельные доставки; если двойная запись навредит, добавьте вторую защиту по type плюс data.id плюс data.updated_at.
Гарантии порядка нет. Доставки уходят в порядке возрастания срока, но повтор раннего события может прийти после более позднего. Перед перезаписью ссылки сравнивайте data.updated_at с тем, что у вас уже сохранено, и не опирайтесь на timestamp обертки для определения порядка: у него точность только до одной секунды.
Где живут данные о кликах вместо webhook на клик
Это та часть, в которой старая версия этого поста ошибалась. Сегодня webhook click не существует, а click.created запланирован, но не выпущен. Путь редиректа намеренно остается свободным от синхронной работы, что объясняет статья приемо-сдаточная (fire-and-forget) обработка кликов, и клики уходят в хранилище аналитики, а не в очередь webhook.
Для данных на уровне кликов уже сегодня есть два пути:
- Пересыльщики событий. Каждый клик по короткой ссылке становится серверным событием в инструменте, которым вы уже пользуетесь: события кликов Mixpanel по ссылкам, события кликов Klaviyo в профилях или метрики редиректов Datadog для операционных дашбордов.
- API аналитики.
GET /v1/analytics/workspaces/{workspace_id}/clicks/recentвозвращает недавние клики, аclicks.csvэкспортирует их, так что запланированная задача может забирать нужные строки.
Что подойдет, зависит от задержки и того, куда в итоге попадают данные; этот компромисс разбирает статья webhook против опроса для отслеживания кликов. А если выборочный поток click.created будет выпущен, эта страница сообщит об этом первой.
Прочитайте опорную статью: умные ссылки: как это работает.
По теме в блоге
- Webhook против опроса для отслеживания кликов - когда отправлять, а когда запрашивать.
- Быстрый старт API + SDK URL-шортенера - входящая поверхность API.
- Приемо-сдаточная обработка кликов - почему клики никогда не ждут исходящих вызовов.
- События кликов Mixpanel по ссылкам - данные о кликах как серверные события.
- Метрики редиректов ссылок Datadog - здоровье редиректа на операционном дашборде.
- Проверка подписей webhook - проверки HMAC в Node, Python, Go и n8n, а также отладка несовпадения.
Частые вопросы
Отправляет ли Elido webhook на каждый клик по ссылке?
Пока нет. 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 и пропускайте любой запрос, значение которого вы уже обработали. Оно идентифицирует одну доставку на один endpoint и остается одинаковым при автоматических повторах и ручных повторных отправках, так что уникального индекса по нему достаточно.
Кто может создавать или удалять webhook в рабочем пространстве?
Администраторы и владельцы рабочего пространства. Для просмотра списка endpoint и журнала доставки нужен доступ на просмотр; для создания, редактирования, удаления, ротации секрета или повторной отправки доставки нужно разрешение workspace.edit. API-ключи ограничены ролью, выбранной при создании ключа.
Попробуйте Elido
Вставьте URL - получите короткую ссылку
Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.
Бесплатно, без регистрации · 2 в день