What you'll set up
- An endpoint that receives Elido events such as
link.created,link.updated,link.deleted,member.invitedanddomain.verified. - HMAC-SHA256 verification of the
X-Elido-Signatureheader, so your server can confirm each delivery came from Elido. - Three delivery attempts per event, a delivery log with a manual Retry button, and secret rotation with a 7-day overlap.
Webhooks push events from Elido to your server as they happen, so you don't have to poll the API. Typical uses: syncing link metadata to a CRM, keeping an internal index of links up to date, or starting a job when a link is published.
Webhooks cover changes to links, domains, members and the workspace. They don't carry individual clicks. For click data, use the click export or an event integration such as Mixpanel. Per-click webhook events are planned.
Add a webhook endpoint
- Go to Dashboard → Webhooks.
- Enter the URL we should POST to and an optional description.
- Choose the endpoint type: Events (you pick event types) or SIEM (firehose) (every event in the workspace).
- For an Events endpoint, tick the event types you want. The
link.*events are ticked by default. - Click Add endpoint. The signing secret appears once, right under the form. Copy it now: Elido won't show it again.
The URL must be HTTPS. Plain http:// is rejected when you save the endpoint, except for localhost and 127.0.0.1, which stay allowed for local development.
You can pause and resume an endpoint from the list. A paused endpoint gets no new deliveries, and anything still queued for it waits until you resume.
Who can manage webhooks
Anyone in the workspace can view endpoints and the delivery log. Creating, editing, pausing, deleting, rotating the secret and retrying a delivery need the workspace.edit permission, which the built-in Admin and Owner roles have. An API key can call the same webhook endpoints of the API if its role has that permission.
Endpoint types
- Events (
event): gets only the event types you subscribed to. Signed. - SIEM (
siem): gets every event published for the workspace, includingaudit.eventfor each audit-log entry. Signed. Meant for log pipelines and security tools. - Discord, Telegram and Sentry: chat and alerting channels that reuse the same delivery system but send the vendor's own message format. They authenticate through the vendor URL or token, so they carry no signature headers. See Discord, Telegram and Sentry.
Everything below about signatures and secrets applies to Events and SIEM endpoints only.
Event catalogue
Names follow <resource>.<action>. The dashboard form offers the most common ones; through the API you can subscribe an Events endpoint to any type in this list, and a SIEM endpoint receives all of them.
In the dashboard picker:
link.created: a short link was created, including links created in bulk.link.updated: a link changed (destination, title, tags, expiry and so on), was restored, had its status changed, or had its safety status changed by moderation.link.deleted: a link was soft-deleted.workspace.updated: workspace settings changed.workspace.created: published when a workspace is created. Because no endpoint can exist in a workspace before it is created, you won't normally receive this one.member.invited: someone joined the workspace, by direct add, accepted invitation or SCIM provisioning.member.removed: a member was removed, by hand or through SCIM.audit.event: one event per audit-log entry.
Available through the API or a SIEM endpoint:
link.expired: a link passed its expiry date.link.cap_reached: a link hit its click cap.dataincludescapandclicks.link.broken: Elido's link-health check found the destination failing.dataincludesstatus_code,error_messageandfirst_seen_at.link.encrypted_createdandlink.encryption_rotated: for encrypted links.domain.verified: a custom domain passed its DNS check.domain.ssl_failed: a custom domain's DNS records were still not found 24 hours after it was added. Despite the name, it's about DNS verification, not certificate renewal.member.role_changed: a member's role changed.dataincludesold_role,new_roleandchanged_by_user_id.invitation.createdandinvitation.accepted.- Billing events:
subscription.*(for examplesubscription.activated,subscription.renewed,subscription.canceled) andusage.threshold_80,usage.threshold_100.
Planned, not sent today: link.clicked.aggregated (click counts per link per window), per-click events, conversion.recorded, domain.tls_renewed, qr.generated and a separate abuse.flagged event. A moderation decision on a link currently arrives as link.updated with the new abuse_status.
Payload shape
Every signed delivery is a POST with Content-Type: application/json, User-Agent: Elido-Webhooks/1.0 and this envelope:
{
"type": "link.created",
"workspace_id": 4123,
"timestamp": "2026-09-22T09:42:11Z",
"data": {
"id": 891234,
"workspace_id": 4123,
"domain_id": 12,
"slug": "spring-2026",
"destination_url": "https://acme.com/spring-sale",
"title": "Spring sale",
"tags": ["email"],
"status": "active",
"expires_at": null,
"created_by_user_id": 5821,
"created_at": "2026-09-22T09:42:11Z"
}
}
typeis the event type. It's also in theX-Webhook-Eventheader.timestampis when the event happened, in UTC.datadepends on the event. Forlink.*events it's the full link object (the example above is trimmed). For other events it's a small object with the IDs and fields listed in the catalogue.- Secrets never leave Elido: fields such as a link's password hash or an invitation token are removed from
databefore delivery.
There's no event id field in the body yet; a per-event ID is planned. For idempotency use the X-Webhook-Delivery header, described below.
Headers and signatures
Each delivery to an Events or SIEM endpoint carries:
| Header | Value |
|---|---|
X-Elido-Signature | v1= followed by the hex HMAC-SHA256 signature |
X-Webhook-Signature | Same value as X-Elido-Signature, kept for older integrations |
X-Webhook-Timestamp | Unix time in seconds when this request was signed |
X-Webhook-Event | The event type, for example link.created |
X-Webhook-Delivery | The delivery ID. It stays the same across retries of the same delivery |
X-Elido-Signature-Previous | Only during a secret rotation: the same signature made with the old key |
The signature is computed as:
v1=hex( HMAC-SHA256( secret, "{X-Webhook-Timestamp}.{raw request body}" ) )
The key is your whole secret string, whsec_ prefix included, used as-is. Sign the raw bytes you received, not a re-serialized JSON object, and compare in constant time. A short Node example (Express):
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.ELIDO_WEBHOOK_SECRET; // full whsec_... value
function same(a, b = "") {
const x = Buffer.from(a);
const y = Buffer.from(b);
return x.length === y.length && timingSafeEqual(x, y);
}
const app = express();
app.post(
"/webhooks/elido",
express.raw({ type: "application/json" }),
(req, res) => {
const ts = req.get("X-Webhook-Timestamp") ?? "";
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.status(400).send("stale timestamp");
}
const expected =
"v1=" +
createHmac("sha256", SECRET)
.update(`${ts}.`)
.update(req.body)
.digest("hex");
const ok = [
req.get("X-Elido-Signature"),
req.get("X-Elido-Signature-Previous"),
].some((sig) => same(expected, sig));
if (!ok) return res.status(401).send("bad signature");
const event = JSON.parse(req.body.toString("utf8"));
// queue event for processing, then answer quickly
res.sendStatus(200);
},
);
For Python and Go versions, a test vector and the common mistakes, see how to verify webhook signatures.
Rotating the signing secret
Open the endpoint in Dashboard → Webhooks and click Rotate secret. The new whsec_ secret is shown once, and Elido signs with it from then on.
The old key stays valid for a 7-day grace window. During that window each delivery carries two signatures: X-Elido-Signature with the new key and X-Elido-Signature-Previous with the old one, so a receiver that hasn't switched yet keeps verifying. Accept either header while you roll the new secret out.
Endpoints created before 22 September 2026 are one exception: their first rotation produces no X-Elido-Signature-Previous, so the original secret stops working straight away. Deploy the new secret right after that first rotation. Endpoints created since then dual-sign from the first rotation onwards. From the second rotation on, the 7-day overlap applies.
Retries
A delivery fails on any non-2xx response, a network error, or no response within 10 seconds. Each delivery gets 3 attempts in total:
- The first attempt, a few seconds after the event.
- A retry about 5 minutes after the first failure.
- A last retry about 15 minutes after the second failure.
If the third attempt fails, the delivery is marked failed and isn't retried again automatically. The endpoint stays active and new events keep being delivered. Your endpoint can see the same delivery more than once (for example, when it processed a request but answered after the 10-second timeout), so dedupe on X-Webhook-Delivery.
Delivery log
Open an endpoint in Dashboard → Webhooks to see its most recent 100 deliveries. For each one you get the event type, status (pending, delivered or failed), the HTTP status your server returned, response time, attempts used out of 3, when it was created and finished, the next retry time for pending ones, the last error, and the full payload that was sent.
Any delivered or failed delivery has a Retry button. It queues the same delivery again (same X-Webhook-Delivery ID, freshly signed) with a new budget of 3 attempts. Through the API, GET /v1/workspaces/{workspace_id}/webhooks/{id}/deliveries pages through older deliveries with limit (up to 100) and offset, and POST .../deliveries/{delivery_id}/retry does the same as the button. If you can't replay a delivery yourself, Elido support can replay it for you.
Recording your server's response body in the log is planned.
Local testing
A CLI command that forwards webhooks to localhost (npx @elido/cli webhooks forward) is planned. Until then, expose your local server through a tunnel such as ngrok or Cloudflare Tunnel, add the tunnel's HTTPS URL as an endpoint, and remove it when you're done.
Limits
- 10 seconds for your endpoint to respond.
- 3 delivery attempts per event, plus manual retries.
- The delivery log API returns at most 100 deliveries per page.
Troubleshooting
Signature never matches. The usual causes: your framework parsed and re-serialized the JSON before you hashed it (hash the raw body), you dropped the whsec_ prefix from the key, or you're signing with the body's timestamp instead of the X-Webhook-Timestamp header. The signature verification post has a test vector to check your code against.
Signature stopped matching after a rotation. Your server still has the old secret. Deploy the new one. On an endpoint created before 22 September 2026, the first rotation has no X-Elido-Signature-Previous to fall back on.
Some events arrive twice. Elido retried because your endpoint answered with an error or took longer than 10 seconds. Dedupe on X-Webhook-Delivery, and answer with a 2xx before doing slow work.
An event type never arrives. Check that the endpoint is subscribed to it and isn't paused, and that the type is in the catalogue above. Types marked planned aren't sent yet.