Folosește webhook-uri când un om sau un flux de lucru trebuie să reacționeze în câteva secunde și fă polling când câteva minute de întârziere sunt acceptabile și preferi să nu rulezi un endpoint public. La Elido alegerea are o capcană: webhook-urile pentru urmărirea clicurilor nu există încă. Webhook-urile acoperă evenimente de link, domeniu, membru și spațiu de lucru, iar un eveniment click.created per clic este planificat, dar nu este emis. Astăzi datele despre clicuri vin din polling pe API-ul de analiză.
Acest articol face calculul pentru întârziere și volumul de cereri, trece prin ce garantează fiecare model (retry-uri, duplicate, ordine, semnături) și se încheie cu un tabel de decizie. Codul a fost rulat în Node 22 pe mock-uri locale înainte să intre aici. Pentru catalogul complet de evenimente de link, citește webhook-uri pentru evenimente de link; pentru endpoint-urile de clicuri, ghidul API-ului de analiză a linkurilor.
Webhook-uri vs polling, un paragraf pentru fiecare
Polling înseamnă că codul tău cere date noi după un program. Un job cron apelează GET /v1/workspaces/{workspace_id}/analytics/clicks/recent, procesează ce este nou și adoarme. Tu inițiezi fiecare cerere. Nu ai nevoie de un URL public.
Webhook-urile inversează direcția. Elido trimite un POST semnat cu un plic JSON către endpoint-ul tău HTTPS când se declanșează un eveniment, iar un răspuns 2xx marchează livrarea ca încheiată. Ai nevoie de un endpoint accesibil. TLS. Un handler care supraviețuiește duplicatelor. Astăzi evenimentele sunt link.created, link.updated, link.expired, domain.verified și restul listei din documentația webhook-urilor.
Calculul de latență și cost
Întârzierea la polling este fixată de interval. Cu un interval de I minute, un eveniment așteaptă în medie I/2 minute și în cel mai rău caz I minute, plus orice întârziere are datele până devin interogabile. Cererile pe zi sunt 1,440 / I.
- 1 minut: 1,440 de cereri pe zi, 30 de secunde întârziere medie, 1 minut în cel mai rău caz.
- 5 minute: 288 de cereri, 2.5 minute în medie, 5 minute în cel mai rău caz.
- 15 minute: 96 de cereri, 7.5 minute în medie, 15 minute în cel mai rău caz.
- 60 de minute: 24 de cereri, 30 de minute în medie, 60 de minute în cel mai rău caz.
Costul apare ca cereri irosite, nu ca presiune pe limita de rată. Limita publicată pentru spațiul de lucru pe Free este de 10 cereri pe secundă, adică 864,000 pe zi (vezi limitele de rată ale API-ului și idempotența). Un singur poller la interval de 1 minut folosește 1,440 / 864,000 = 0.17% din ea. Vei atinge limitele când faci polling per link pe sute de linkuri sau când alte automatizări împart spațiul de lucru, nu cu un singur flux.
Irosirea este cifra care contează. Spune că un spațiu de lucru modifică 40 de linkuri pe zi, iar tu urmărești asta prin polling în fiecare minut. Cel mult 40 din 1,440 de cereri returnează ceva nou, deci cel puțin 1,400 (97%) sunt goale. Un receptor de webhook primește 40 de cereri pentru aceleași 40 de evenimente. În cel mai rău caz, cu trei încercări fiecare, sunt 120.
Webhook-urile câștigă la întârziere, care este de obicei de ordinul secundelor, pentru că workerul de livrare preia continuu rândurile în așteptare. Pierd la ce primește polling-ul gratis: o rulare ratată este reparată de rularea următoare, pentru că datele sunt încă în API. Un webhook ratat este reparat doar dacă ai construit ceva care să-l repare.
Garanții de livrare: ce promite Elido
Aceste comportamente provin din codul de livrare și din documentația de ajutor, verificate la 1 octombrie 2026.
Retry-uri. O livrare primește trei încercări: una imediat, una la 5 minute după un eșec, una la 15 minute după aceea, în total aproximativ 20 de minute. Orice status non-2xx, eroare de rețea sau răspuns mai lent de 10 secunde este un eșec. După a treia, livrarea este marcată ca eșuată; Retry din tabloul de bord sau POST .../deliveries/{delivery_id}/retry o rearmează cu un buget proaspăt de trei încercări. Pentru comparație, Stripe reîncearcă evenimentele în modul live până la trei zile. Douăzeci de minute sunt puține. Nu presupune că o întrerupere a receptorului va fi acoperită.
Idempotență. Livrarea este cel puțin o dată. Antetul X-Webhook-Delivery identifică o livrare către un endpoint și nu se schimbă la retry-urile automate sau manuale, deci este cheia ta de deduplicare.
Ordine. Nu există nicio promisiune de ordine. Un retry al unui eveniment mai vechi poate sosi după unul mai nou, așa că compară data.updated_at cu ce ai stocat înainte să suprascrii ceva.
Semnături. Fiecare livrare semnată poartă X-Webhook-Signature (alias X-Elido-Signature) cu v1= plus HMAC-SHA256 în hex al lui {timestamp}.{raw_body}, iar timestamp-ul este în X-Webhook-Timestamp. Timestamp-ul este nou la fiecare încercare, deci un retry legitim nu este niciodată expirat. În timpul unei rotiri de secret, X-Elido-Signature-Previous poartă o semnătură făcută cu vechiul secret timp de șapte zile. Verifică semnăturile webhook acoperă Python, Go și depanarea unei nepotriviri.
Transport și pauză. Endpoint-urile noi de evenimente trebuie să folosească https; http simplu este acceptat doar pentru localhost și 127.0.0.1. Setarea is_active pe false pune un endpoint pe pauză. De la corecția din 22 septembrie, asta oprește și livrările din coadă.
Un receptor de webhook în Node
Acest receptor verifică semnătura pe octeții bruți, impune o fereastră de cinci minute, acceptă antetul cu secretul anterior în timpul rotirii și deduplică după ID-ul livrării. Toleranța de cinci minute este alegerea ta, nu a Elido, și corespunde valorii implicite Stripe. L-am rulat pe un semnatar local: o cerere validă a returnat 200, o reluare cu același ID de livrare a returnat "duplicate", un secret greșit și un timestamp vechi de 400 de secunde au returnat ambele 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);
Verificarea lungimii înainte de timingSafeEqual contează. Node aruncă excepție pentru buffere de dimensiuni diferite. Răspunde întâi, lucrează după. Limita de 10 secunde este locul în care un handler lent, dar reușit, devine un duplicat.
Un poller de clicuri în Node
Până când click.created este livrat, acesta este drumul clicurilor. clicks/recent returnează cele mai noi întâi, acceptă limit până la 500 (implicit 100) și returnează next_cursor ori de câte ori o pagină vine plină. Fiecare rulare citește de sus și se oprește când ajunge la cel mai nou clic pe care l-a procesat deja. L-am rulat pe un mock care paginează câte două rânduri: prima rulare a procesat 5 clicuri, a doua doar cele 2 noi, a treia niciunul.
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);
Două limite, spuse direct. Cursorul ordonează după timestamp și ID-ul linkului, deci două clicuri pe același link în aceeași secundă împart o cheie; dacă nu îți permiți un clic rar omis sau repetat, fă handle idempotent pe rândul întreg. Iar lastSeen trăiește în memorie, deci persistă-l între reporniri, altfel o repornire reia istoricul recent.
Nu ești gata să construiești niciuna? E în regulă. Deschide tabloul de bord și folosește mai întâi analiza din tabloul de bord, apoi adaugă API-ul când știi ce cifre îți trebuie după program.
Tabel de decizie
| Situație | Folosește astăzi | De ce |
|---|---|---|
| Reacție la schimbări de link sau domeniu în secunde | Webhook pe link.*, domain.* | Evenimentele sunt împinse; nu e nevoie de un eveniment de clic |
| Contor live de clicuri în produsul tău | Polling la 1 minut pe clicks/recent | Încă nu există webhook de clic; 1,440 de cereri pe zi încap bine în limită |
| Îmbogățire CRM per clic | Event forwarder sau polling scurt | O întârziere medie de 30 de secunde la polling este de obicei acceptabilă |
| Raportare săptămânală sau lunară | Polling pe summary și timeseries | Datele retrospective nu câștigă nimic din push |
| Niciun endpoint public disponibil | Polling | Webhook-urile cer un URL https accesibil de pe internet |
| Nu poți pierde un eveniment de link | Webhook plus reconciliere nocturnă | Retry-urile acoperă aproximativ 20 de minute, polling-ul acoperă restul |
Când va fi livrat fluxul eșantionat click.created, rândurile pentru contorul live și CRM trec în coloana webhook, iar polling-ul devine pasul de reconciliere. Până atunci, event forwarderele precum evenimentele de clic Mixpanel sunt opțiunea de tip push pentru datele de clic.
Modelul hibrid
Pentru orice nu îți permiți să pierzi, rulează ambele. Webhook-ul este drumul rapid. Un polling programat compară totalurile și găsește golurile: livrări care și-au epuizat cele trei încercări în timpul unei întreruperi sau evenimente sosite în altă ordine.
Rapoartele summary și timeseries returnează totaluri și găleți zilnice pentru un interval de date într-un singur apel, ceea ce face o comparație nocturnă ieftină. Ține minte că filtrul de boți elimină crawlerele și monitoarele înainte ca clicurile să fie înregistrate, deci totalurile tale nu vor corespunde jurnalelor brute ale serverului; ghidul de analiză explică ce este exclus. Lista de verificare de securitate acoperă restul. Dacă îți proiectezi propriile webhook-uri, specificația Standard Webhooks este o referință utilă.
Ecranul de documentație API al unui spațiu de lucru DEMO cu date de exemplu; nu este un cont de client.
Alte articole de pe blog
- Webhook-uri pentru evenimente de link: payload-uri, semnături, retry-uri
- Verifică semnăturile webhook: HMAC-SHA256 în Node, Python, Go
- Limitele de rată, retry-urile și idempotența API-ului unui URL shortener
- API de analiză a linkurilor: extrage statistici de clicuri cu o cheie API
- Ingestie de clicuri fire-and-forget
- Ghid rapid pentru API-ul și SDK-urile unui URL shortener
Întrebări frecvente
Care este diferența dintre webhook-uri și polling?
La polling, codul tău cere API-ului date noi după un program. La webhook-uri, serverul trimite un HTTP POST către endpoint-ul tău când se întâmplă ceva. Polling-ul costă cereri și adaugă până la un interval de întârziere. Webhook-urile costă un endpoint public mereu activ și munca de a gestiona retry-urile și duplicatele.
Are Elido webhook-uri pentru urmărirea clicurilor?
Încă nu. Webhook-urile acoperă evenimente de link, domeniu, membru și spațiu de lucru, precum link.created și link.expired. Un eveniment click.created este planificat, dar nimic nu îl publică astăzi. Pentru date la nivel de clic, interoghează periodic raportul clicks/recent al API-ului de analiză cu cursorul lui sau folosește un event forwarder.
Cât de des ar trebui să interoghez periodic un API de analiză?
Potrivește intervalul cu decizia pe care o alimentează datele. Un minut se potrivește unui contor aproape live, de cinci până la cincisprezece minute se potrivește tablourilor de bord, iar din oră în oră sau zilnic se potrivește rapoartelor. Un polling la 1 minut face 1,440 de cereri pe zi, iar unul la 5 minute face 288, deci un interval mai scurt merită rar.
Sunt webhook-urile garantat livrate exact o dată?
Nu. Livrarea webhook-urilor este cel puțin o dată, deci un receptor vede duplicate după un timeout sau o retrimitere manuală. Elido păstrează aceeași valoare X-Webhook-Delivery la toate retry-urile, deci stochează acel ID și ignoră tot ce ai tratat deja.
Ce se întâmplă dacă endpoint-ul meu de webhook este oprit?
Elido face trei încercări: una imediat, una la cinci minute după un eșec și una la cincisprezece minute după aceea. Dacă toate trei eșuează, livrarea este marcată ca eșuată până când cineva apasă Retry sau apelează endpoint-ul de retry. Întreruperile mai lungi de aproximativ douăzeci de minute au nevoie de un job de reconciliere.
Pot combina webhook-urile și polling-ul?
Da, și pentru orice contează ar trebui. Folosește webhook-ul ca drum rapid și un polling programat ca plasă de siguranță care găsește evenimentele pe care receptorul tău le-a ratat. Polling-ul poate rula din oră în oră sau noaptea, pentru că trebuie doar să închidă golurile.
Încearcă Elido
Lipește un URL, obții un link scurt funcțional
Fără înregistrare. Linkul este activ timp de 30 de zile. Înregistrează-te ca să-l păstrezi pentru totdeauna.
Gratuit, fără înregistrare · 2 pe zi