Gebruik webhooks wanneer een persoon of een workflow binnen enkele seconden moet reageren, en pol wanneer een paar minuten vertraging prima is en je liever geen openbaar endpoint draait. Bij Elido zit er een addertje onder de keuze: webhooks voor klikregistratie bestaan nog niet. Webhooks dekken link-, domein-, leden- en werkruimte-events, en een click.created-event per klik is gepland maar wordt niet uitgezonden. Klikdata komt vandaag uit het pollen van de analytics-API.
Dit bericht rekent door wat vertraging en requestvolume betekenen, behandelt wat elk patroon garandeert (retries, duplicaten, volgorde, handtekeningen) en eindigt met een beslistabel. De code is in Node 22 tegen lokale mocks gedraaid voordat hij hierin kwam. Voor de volledige catalogus van linkevents, lees webhooks voor linkevents; voor de klik-endpoints, de gids voor de linkanalytics-API.
Webhooks vs polling in één alinea per patroon
Polling betekent dat je code volgens een schema om nieuwe data vraagt. Een cronjob roept GET /v1/workspaces/{workspace_id}/analytics/clicks/recent aan, verwerkt wat nieuw is en gaat slapen. Jij start elk request. Een openbare URL is niet nodig.
Webhooks keren de richting om. Elido stuurt een ondertekende POST met een JSON-envelop naar je HTTPS-endpoint wanneer een event afgaat, en een 2xx-respons markeert de bezorging als afgerond. Je hebt een bereikbaar endpoint nodig. TLS. Een handler die duplicaten overleeft. Vandaag zijn de events link.created, link.updated, link.expired, domain.verified en de rest van de lijst in de webhookdocumentatie.
Het rekenwerk voor vertraging en kosten
De pollvertraging wordt bepaald door het interval. Bij een interval van I minuten wacht een event gemiddeld I/2 minuten en in het slechtste geval I minuten, plus de vertraging die de data heeft voordat hij opvraagbaar is. Requests per dag zijn 1.440 / I.
- 1 minuut: 1.440 requests per dag, gemiddeld 30 seconden vertraging, 1 minuut in het slechtste geval.
- 5 minuten: 288 requests, gemiddeld 2,5 minuten, 5 minuten in het slechtste geval.
- 15 minuten: 96 requests, gemiddeld 7,5 minuten, 15 minuten in het slechtste geval.
- 60 minuten: 24 requests, gemiddeld 30 minuten, 60 minuten in het slechtste geval.
De kosten zitten in verspilde requests, niet in druk op de rate limit. De gepubliceerde werkruimtelimiet op Free is 10 requests per seconde, wat 864.000 per dag is (zie API-rate limits en idempotentie). Eén poller met een interval van 1 minuut gebruikt 1.440 / 864.000 = 0,17% daarvan. Je loopt tegen limieten aan wanneer je per link pollt over honderden links, of wanneer andere automatisering de werkruimte deelt, niet met één feed.
De verspilling is het getal dat telt. Stel dat een werkruimte 40 links per dag wijzigt en je dat in de gaten houdt door elke minuut te pollen. Hooguit 40 van de 1.440 requests leveren iets nieuws op, dus minstens 1.400 (97%) zijn leeg. Een webhookontvanger krijgt 40 requests voor dezelfde 40 events. In het slechtste geval, met drie pogingen elk, zijn dat er 120.
Webhooks winnen op vertraging, die doorgaans seconden is omdat de bezorgworker voortdurend wachtende rijen oppakt. Ze verliezen op wat polling gratis krijgt: een gemiste run wordt hersteld door de volgende run, omdat de data nog in de API staat. Een gemiste webhook wordt alleen hersteld als je iets hebt gebouwd om dat te doen.
Bezorggaranties: wat Elido belooft
Dit gedrag komt uit de bezorgcode en de helpdocumentatie, gecontroleerd op 1 oktober 2026.
Retries. Een bezorging krijgt drie pogingen: één direct, één 5 minuten na een mislukking, één 15 minuten daarna, ongeveer 20 minuten van begin tot eind. Elke niet-2xx-status, netwerkfout of respons langzamer dan 10 seconden telt als mislukking. Na de derde wordt de bezorging als mislukt gemarkeerd; Retry in het dashboard of POST .../deliveries/{delivery_id}/retry zet hem opnieuw klaar met een vers budget van drie pogingen. Ter vergelijking: Stripe doet retries voor live-mode-events tot drie dagen. Twintig minuten is kort. Ga er niet vanuit dat een storing aan de kant van de ontvanger wordt opgevangen.
Idempotentie. Bezorging is at least once. De header X-Webhook-Delivery identificeert één bezorging aan één endpoint en verandert niet bij automatische of handmatige retries, dus het is je dedupe-sleutel.
Volgorde. Er is geen belofte over volgorde. Een retry van een vroeg event kan na een later event aankomen, dus vergelijk data.updated_at met wat je hebt opgeslagen voordat je iets overschrijft.
Handtekeningen. Elke ondertekende bezorging draagt X-Webhook-Signature (alias X-Elido-Signature) met v1= plus de hex-HMAC-SHA256 van {timestamp}.{raw_body}, en de timestamp in X-Webhook-Timestamp. De timestamp is bij elke poging vers, dus een legitieme retry is nooit verouderd. Tijdens een secret-rotatie draagt X-Elido-Signature-Previous zeven dagen lang een handtekening van het oude secret. Webhookhandtekeningen verifiëren behandelt Python, Go en het debuggen van een mismatch.
Transport en pauzeren. Nieuwe event-endpoints moeten https gebruiken; gewoon http wordt alleen geaccepteerd voor localhost en 127.0.0.1. Als je is_active op false zet, pauzeert dat een endpoint. Sinds de fix van 22 september stopt dat ook wachtende bezorgingen.
Een webhookontvanger in Node
Deze ontvanger verifieert de handtekening over de ruwe bytes, handhaaft een venster van vijf minuten, accepteert de header met het vorige secret tijdens rotatie en dedupliceert op het bezorg-ID. De tolerantie van vijf minuten is jouw keuze, niet die van Elido, en komt overeen met de standaardwaarde van Stripe. Ik heb hem gedraaid tegen een lokale ondertekenaar: een geldig request gaf 200, een replay met hetzelfde bezorg-ID gaf "duplicate", en een verkeerd secret en een timestamp van 400 seconden oud gaven allebei 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);
De lengtecontrole vóór timingSafeEqual doet ertoe. Node gooit een fout bij buffers van verschillende grootte. Antwoord eerst, werk daarna. De cut-off van 10 seconden is het punt waarop een trage maar geslaagde handler een duplicaat wordt.
Een klikpoller in Node
Totdat click.created verschijnt, is dit het klikpad. clicks/recent geeft de nieuwste eerst terug, accepteert limit tot 500 (standaard 100) en geeft next_cursor terug zodra een pagina vol is. Elke run leest vanaf de bovenkant en stopt wanneer hij de nieuwste klik bereikt die hij al heeft afgehandeld. Ik heb hem gedraaid tegen een mock die twee rijen per keer pagineert: de eerste run handelde 5 klikken af, de tweede alleen de 2 nieuwe, de derde geen enkele.
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);
Twee beperkingen, kort en eerlijk. De cursor sorteert op timestamp en link-ID, dus twee klikken op één link in dezelfde seconde delen een sleutel; kun je het je niet veroorloven dat een klik zelden wordt overgeslagen of herhaald, maak handle dan idempotent op de volledige rij. En lastSeen leeft in het geheugen, dus bewaar hem tussen herstarts, anders speelt een herstart recente geschiedenis opnieuw af.
Nog niet klaar om een van beide te bouwen? Dat is prima. Open het dashboard en gebruik eerst de dashboardanalytics, en voeg de API toe wanneer je weet welke cijfers je volgens een schema nodig hebt.
Beslistabel
| Situatie | Gebruik vandaag | Waarom |
|---|---|---|
| Binnen seconden reageren op link- of domeinwijzigingen | Webhook op link.*, domain.* | Events worden gepusht; een klikevent is niet nodig |
| Live klikteller in je product | Poll van 1 minuut op clicks/recent | Nog geen klikwebhook; 1.440 requests per dag valt ruim binnen de limiet |
| CRM-verrijking per klik | Event forwarder of korte poll | Een pollvertraging van gemiddeld 30 seconden is meestal acceptabel |
| Wekelijkse of maandelijkse rapportage | Poll summary en timeseries | Terugblikkende data wint niets bij push |
| Geen openbaar endpoint beschikbaar | Polling | Webhooks vragen om een https-URL die vanaf internet bereikbaar is |
| Een linkevent mag niet verloren gaan | Webhook plus nachtelijke reconciliatie | Retries dekken ongeveer 20 minuten, de poll dekt de rest |
Zodra de gesamplede click.created-stroom verschijnt, schuiven de rijen voor de live teller en het CRM naar de webhookkolom en wordt de poll de reconciliatieronde. Tot die tijd zijn event forwarders zoals Mixpanel-linkklikevents de push-achtige optie voor klikdata.
Het hybride patroon
Draai voor alles wat je niet kunt missen allebei. De webhook is de snelle route. Een geplande poll vergelijkt totalen en vindt gaten: bezorgingen die hun drie pogingen tijdens een storing hebben opgebruikt, of events die in de verkeerde volgorde aankwamen.
De rapporten summary en timeseries geven totalen en dagelijkse buckets voor een datumbereik in één aanroep, wat een nachtelijke vergelijking goedkoop maakt. Bedenk dat het botfilter crawlers en monitors verwijdert voordat klikken worden vastgelegd, dus je totalen komen niet overeen met ruwe serverlogs; de analyticsgids legt uit wat wordt uitgesloten. De beveiligingschecklist behandelt de rest. Ontwerp je zelf webhooks, dan is de Standard Webhooks-spec een nuttige referentie.
Het API-documentatiescherm van een DEMO-werkruimte met voorbeelddata; het is geen klantaccount.
Gerelateerd op de blog
- Webhooks voor linkevents: payloads, handtekeningen, retries
- Webhookhandtekeningen verifiëren: HMAC-SHA256 in Node, Python, Go
- API-rate limits, retries en idempotentie van een URL-shortener
- Linkanalytics-API: klikstatistieken ophalen met een API-sleutel
- Fire-and-forget klikverwerking
- Quickstart voor de URL-shortener-API en SDK's
Veelgestelde vragen
Wat is het verschil tussen webhooks en polling?
Bij polling vraagt jouw code de API volgens een schema om nieuwe data. Bij webhooks stuurt de server een HTTP POST naar jouw endpoint wanneer er iets gebeurt. Polling kost requests en voegt tot één interval aan vertraging toe. Webhooks kosten een altijd bereikbaar openbaar endpoint en het werk om retries en duplicaten af te handelen.
Heeft Elido webhooks voor klikregistratie?
Nog niet. Webhooks dekken link-, domein-, leden- en werkruimte-events zoals link.created en link.expired. Een click.created-event is gepland, maar er publiceert vandaag niets naar. Voor data op klikniveau pol je het rapport clicks/recent van de analytics-API met zijn cursor, of gebruik je een event forwarder.
Hoe vaak moet ik een analytics-API pollen?
Stem het interval af op de beslissing waar de data in uitmondt. Eén minuut past bij een bijna live teller, vijf tot vijftien minuten bij dashboards, en elk uur of elke dag bij rapporten. Een poll per minuut levert 1.440 requests per dag op en een poll per vijf minuten 288, dus korter is zelden de moeite waard.
Komen webhooks gegarandeerd precies één keer aan?
Nee. Webhookbezorging is at least once, dus een ontvanger ziet duplicaten na een timeout of een handmatige herverzending. Elido behoudt dezelfde X-Webhook-Delivery-waarde bij alle retries, dus sla dat ID op en sla alles over wat je al hebt afgehandeld.
Wat gebeurt er als mijn webhook-endpoint niet bereikbaar is?
Elido doet drie pogingen: één direct, één vijf minuten na een mislukking en één vijftien minuten daarna. Mislukken alle drie, dan wordt de bezorging als mislukt gemarkeerd totdat iemand op Retry drukt of het retry-endpoint aanroept. Storingen die langer duren dan ongeveer twintig minuten vragen om een reconciliatiejob.
Kan ik webhooks en polling combineren?
Ja, en voor alles wat ertoe doet zou je dat moeten doen. Gebruik de webhook als snelle route en een geplande poll als vangnet dat events vindt die je ontvanger heeft gemist. De poll kan elk uur of elke nacht draaien, omdat hij alleen gaten hoeft te dichten.
Probeer Elido
Plak een URL, krijg een werkende korte link
Geen aanmelding nodig. Link blijft 30 dagen actief. Meld je aan om hem voor altijd te bewaren.
Gratis, geen aanmelding nodig · 2 per dag